KoogleApp/docs/IMPLEMENTATION_PLAN.md

68 KiB
Raw Blame History

Koogle App - Analyse & Vorschlag für Bereiche und Pages

Zweck der Anwendung (abgeleitet aus Datenmodell)

Koogle ist eine Vereinsverwaltung für Kegelvereine mit Schwerpunkt auf:

Kernfunktionen

  1. Vereinsverwaltung: Multi-Mandanten-System, jeder Verein = eigener Scope
  2. Mitglieder & Gäste: Verwaltung von Personen mit Status (Member/Guest)
  3. Spieltagsorganisation: Planung und Durchführung von Kegeltagen (Days)
    • Status-Workflow: New → Started → Closed (oder Postponed)
    • Zuordnung von Teilnehmern pro Spieltag
  4. Spielverwaltung: Mehrere Games pro Day möglich
    • JSON-basierte Spielstände (GameData)
    • Teilnehmerzuordnung pro Game
  5. Kostenmanagement:
    • Vordefinierte Kosten/Strafen (Expenses) pro Verein
    • Automatische Trigger (z.B. Pudel, Pumpe, Aus, Kranz, etc.)
    • Variable/Fixe Preise, Inverse Kosten (alle außer einem zahlen)
    • One-Click Kosten für schnelle Erfassung
  6. Abrechnung:
    • Pro Person, pro Tag, pro Spiel
    • Berechnungsmethoden: None, Average, Maximum (für fehlende Personen)
    • Status: Open/Done für PersonExpenses
  7. Benutzer & Berechtigungen:
    • ASP.NET Identity mit Custom UserProfile
    • Rollen pro Verein: Viewer, Editor, Admin
    • SuperAdmin für vereinsübergreifende Verwaltung
    • Multi-Club Zugehörigkeit möglich

gerenelle Implementierungshinweise

  • Der IMPLEMENTATION_PLAN ist in Deutscher Sprache formuliert.
  • Die Anwendung wird in deutscher Sprache implementiert. Alle Klassen, Methoden, Interfaces und sonstigen Artikefakte werden dennoch in englischer Sprache benannt. Auch die XMLDOC Kommentare sind Englisch zu formulieren.
  • Der Anwender wird vom Programm in lockerem Ton und in der Du-Form angesprochen.

Bestehende Implementierung

Vollständig implementiert

  • Authentication/Authorization Framework
  • Login mit Vereinsauswahl
  • Fluxor State Management (AuthState)
  • Rollenbasierte Berechtigungen
  • MudBlazor UI Framework
  • Dual DbContext (Domain + Identity)
  • Clean Architecture Struktur
  • Seeders (SuperAdmin, Roles)

Teilweise implementiert

  • ⚠️ DayService (vorhanden, aber auskommentiert)
  • ⚠️ Navigation (Skelett vorhanden, kaum Menüpunkte)

Nicht implementiert

  • UI für Clubs, Days, Games, Persons, Expenses
  • Services für Person, Game, Expense
  • DTOs für die meisten Domain Entities
  • Fluxor States für Domain-Daten
  • Reporting/Export-Funktionalität

Vorgeschlagene Bereiche & Pages

1. DASHBOARD-BEREICH (Startseite)

Route: / oder /dashboard

Zweck: Übersicht über aktuelle Aktivitäten im ausgewählten Verein

Pages:

  • Dashboard.razor:
    • Nächste geplante Spieltage
    • Aktuelle offene Kosten
    • Quick-Actions (neuer Spieltag, neue Person)
    • Statistiken (Anzahl Mitglieder, Gäste, offene Abrechnungen)

2. SPIELTAG-BEREICH (Day Management)

Route: /days

Pages:

  • DayList.razor (/days):

    • Tabelle: PostDate, Status, Teilnehmeranzahl
    • Filter: Jahr, Monat, Status
    • Actions: Neu, Bearbeiten, Löschen, Schließen
  • DayDetail.razor (/days/{id}):

    • Spieltag-Info (Datum, Status)
    • Teilnehmerliste mit Anwesenheit
    • Games des Tages
    • PersonExpenses des Tages (Übersicht)
    • Actions: Teilnehmer hinzufügen, Spiel hinzufügen, Status ändern
  • DayCreate.razor (/days/new):

    • Formular: Datum, Vorauswahl Teilnehmer aus Mitgliedern
  • DayEdit.razor (/days/{id}/edit):

    • Datum ändern, Teilnehmer hinzufügen/entfernen

3. SPIEL-BEREICH (Game Management)

Route: /days/{dayId}/games

Pages:

  • GameList.razor (/days/{dayId}/games):

    • Liste der Spiele eines Spieltags
    • Actions: Neues Spiel, Bearbeiten, Löschen
  • GameDetail.razor (/days/{dayId}/games/{gameId}):

    • GameData anzeigen/bearbeiten (JSON-Editor oder strukturierte Eingabe)
    • Teilnehmer des Spiels
    • Kosten des Spiels (PersonExpenses mit GameId)
    • Trigger-Events erfassen (z.B. "Pudel" → Expense zuweisen)
  • GameCreate.razor (/days/{dayId}/games/new):

    • Teilnehmer auswählen (aus DayPersons)
    • Optionale GameData-Initialisierung

4. KOSTEN-BEREICH (Expense Management)

Route: /expenses

Pages:

  • ExpenseList.razor (/expenses):

    • Vordefinierte Expenses des Vereins
    • Spalten: Name, Preis, Typ, IsOneClick, IsInverse, IsVariable
    • Actions: Neu, Bearbeiten, Löschen
  • ExpenseCreate.razor (/expenses/new):

    • Formular für neue Expense-Vorlage
    • Trigger-Zuordnung optional
  • PersonExpenseList.razor (/expenses/assigned):

    • Alle zugewiesenen Kosten (PersonExpenses)
    • Filter: Person, Tag, Status (Open/Done), Datum
    • Bulk-Actions: Als bezahlt markieren, Löschen
  • ExpenseTriggerConfig.razor (/expenses/triggers):

    • Trigger-Typen anzeigen
    • Zuordnung Trigger → Expense

5. PERSONEN-BEREICH (Person Management)

Route: /people

Pages:

  • PersonList.razor (/people):

    • Tabelle: Name, Status (Member/Guest), Actions
    • Filter: Status
    • Actions: Neu, Bearbeiten, Löschen
  • PersonCreate.razor (/people/new):

    • Name, Status
  • PersonDetail.razor (/people/{id}):

    • Personen-Info
    • Teilnahme-Historie (DayPersons)
    • Kosten-Historie (PersonExpenses)
    • Statistiken (Gesamtkosten, Anzahl Teilnahmen)

6. AUSWERTUNGEN-BEREICH (Reports/Evaluations)

Route: /reports

Pages:

  • ReportOverview.razor (/reports):

    • Auswahl: Pro Person, Pro Tag, Pro Spiel
  • PersonReport.razor (/reports/person/{id}):

    • Alle Kosten einer Person
    • Summen pro Tag, pro Spiel
    • Zeitraum-Filter
  • DayReport.razor (/reports/day/{id}):

    • Alle Kosten eines Spieltags
    • Aufschlüsselung pro Person
    • Export-Option (PDF, CSV)
  • PeriodReport.razor (/reports/period):

    • Zeitraum wählen (von-bis)
    • Aggregierte Statistiken
    • Top-Spieler, teuerste Tage, etc.

7. STAMMDATEN-BEREICH (Master Data - Vereins-spezifisch)

Route: /masterdata

Pages:

  • ClubSettings.razor (/masterdata/club):

    • Vereins-Name
    • ExpenseCalculation-Methode
    • Weitere Einstellungen
  • ExpenseTemplates.razor (/masterdata/expenses):

    • Siehe Expense-Bereich (evtl. Duplikat)
  • TriggerConfig.razor (/masterdata/triggers):

    • Siehe ExpenseTriggerConfig

8. ADMIN-BEREICH (SuperAdmin - Vereinsübergreifend)

Route: /admin

Pages:

  • ClubList.razor (/admin/clubs):

    • Alle Vereine (SuperAdmin only)
    • Actions: Neu, Bearbeiten, Löschen
  • ClubCreate.razor (/admin/clubs/new):

    • Name, ExpenseCalculation
  • UserManagement.razor (/admin/users):

    • Alle UserProfiles
    • Vereinszuordnung (UserProfileClub)
    • Rollen-Zuordnung pro Verein
  • SystemSettings.razor (/admin/system):

    • Globale Einstellungen

9. PROFIL-BEREICH (User Profile)

Route: /profile

Pages:

  • UserProfile.razor (/profile):
    • DisplayName, Locale, TimeZone ändern
    • Passwort ändern
    • Standard-Verein festlegen (UserProfileClub.IsDefault)
    • Vereins-Mitgliedschaften anzeigen

Vorgeschlagene Navigation-Struktur

NavMenu.razor:
├── 🏠 Dashboard (/)
├── 📅 Spieltage (/days)
├── 👥 Personen (/people)
├── 💰 Kosten
│   ├── Vorlagen (/expenses)
│   ├── Zugewiesen (/expenses/assigned)
│   └── Trigger (/expenses/triggers)
├── 📊 Auswertungen (/reports)
│   ├── Pro Person
│   ├── Pro Tag
│   └── Zeitraum
├── ⚙️ Stammdaten (IsClubEditor+)
│   ├── Verein (/masterdata/club)
│   └── Kosten-Vorlagen
├── 🔧 Admin (IsSuperAdmin)
│   ├── Vereine (/admin/clubs)
│   ├── Benutzer (/admin/users)
│   └── System (/admin/system)
└── 👤 Profil (/profile)

Prioritäts-Vorschlag (Phasen) - basierend auf User-Feedback

Phase 1: MVP - Basis-Verwaltung (DIESE PLANUNG)

Scope: User/Account-Mgmt, Club, Personen (Teilnehmer), Tage, Strafen

  1. User/Account-Verwaltung

    • User-Registrierung (self-service oder Admin)
    • Passwort zurücksetzen
    • User-zu-Club Zuordnung (UserProfileClub)
    • Rollen pro Club zuweisen (UserProfileClubRoleAssignment)
    • User-Profil (DisplayName, Locale, TimeZone)
  2. Club-Verwaltung (SuperAdmin)

    • Liste, Create, Edit, Delete
    • ExpenseCalculation-Methode
  3. Person-Verwaltung (Club-Teilnehmer: Members + Guests)

    • Liste, Create, Edit, Delete
    • Status (Member/Guest)
    • KEINE Login - reine Stammdaten für Kegelclub
  4. Day-Verwaltung

    • Liste, Create, Edit, Delete
    • Datum, Status (New, Started, Closed, Postponed)
    • Teilnehmer zuordnen (DayPerson)
  5. PersonExpense - Strafen manuell erfassen

    • Pro Teilnehmer pro Tag Strafen hinzufügen
    • Expense-Vorlagen (Name, Preis)
    • Status: Open/Done
  6. Dashboard

    • Übersicht: Nächste Tage, offene Kosten
    • Quick-Actions
  7. Einfache Auswertung

    • Pro Tag: Wer schuldet was
    • Pro Person: Summe offener Kosten

Phase 2: Detaillierte Spielverwaltung (SEPARATE PLANUNG SPÄTER)

NICHT in dieser Planung:

  • Wurf-für-Wurf Eingabe
  • Undo-Funktion
  • Plugin-System für verschiedene Kegelspiele
  • GameData strukturiert (JSON für jetzt)
  • Automatische Trigger (Pudel, Pumpe, etc.)
  • Expense-Trigger-Engine

Phase 3: Advanced Features (SPÄTER)

  • Export (PDF, CSV)
  • Benutzer-Rollen verwalten
  • Erweiterte Reports


IMPLEMENTIERUNGSPLAN - Phase 1 MVP

Umsetzungsreihenfolge - Übersicht

Phase Bereich Beschreibung Dateien
A1 Foundation Repository Interfaces 5 Interface-Dateien
A2 Foundation Repository Implementations 5 Repository-Dateien
A3 Foundation DTOs 5 DTO-Dateien
A4 Foundation Service Interfaces 5 Service-Interface-Dateien
A5 Foundation Service Implementations 5 Service-Dateien
A6 Foundation AutoMapper Profiles 5 Mapping-Dateien
A7 Foundation DI Registration 2 DI-Dateien ändern
B1 User/Account UserService erweitern 1 Service, 1 Interface, 1 DTO
B2 User/Account Register Page 1 Razor
B3 User/Account Password Reset Pages 2 Razor
B4 User/Account Profile Page 1 Razor
B5 User/Account Admin Users Page 1 Razor
C1 Clubs ClubState Fluxor 4 State-Dateien
C2 Clubs Club Pages - ERSTES TESTBARES MVP 1 Razor
D1 Personen PersonState Fluxor 4 State-Dateien
D2 Personen Person Pages 1 Razor
D3 Expenses ExpenseState Fluxor 4 State-Dateien
D4 Expenses Expense Pages 1 Razor
E1 Days DayState Fluxor 4 State-Dateien
E2 Days Days List Page 1 Razor
E3 Days Day Details Page 1 Razor
E4 Days PersonExpense Management Components in DayDetails
F1 Dashboard Dashboard Page 1 Razor
F2 Dashboard Evaluation Components 2 Shared Components
F3 Navigation NavMenu finalisieren 1 Razor ändern
G1 Erweiterte Reg. MembershipStatus + UserProfileClub 2 Dateien + Migration
G2 Erweiterte Reg. ClubInvitation Entity 1 Entity + Migration
G3 Erweiterte Reg. IEmailService (Stub) 2 Dateien
G4 Erweiterte Reg. Services erweitern 2 Services
G5 Erweiterte Reg. Dashboard Pending-Widget 1 Component
G6 Erweiterte Reg. Admin Users Page erweitern 1 Razor
G7 Erweiterte Reg. Club-Beitritt UI 1 Razor
G8 Erweiterte Reg. Einladungslink-Handling 2 Dateien

Legende: ☐ = Offen | ☑ = In Arbeit | ✓ = Fertig

Geschätzte Dateien insgesamt: ~90 Dateien


Detaillierte Phasen

Phase A1: Repository Interfaces erstellen

Dateien:

  1. src/Koogle.Domain/Interfaces/IClubRepository.cs
  2. src/Koogle.Domain/Interfaces/IPersonRepository.cs
  3. src/Koogle.Domain/Interfaces/IExpenseRepository.cs
  4. src/Koogle.Domain/Interfaces/IDayRepository.cs (erweitern)
  5. src/Koogle.Domain/Interfaces/IPersonExpenseRepository.cs

Methoden pro Interface: GetAllAsync/GetByClubIdAsync, GetByIdAsync, AddAsync, UpdateAsync, DeleteAsync


Phase A2: Repository Implementations erstellen

Dateien:

  1. src/Koogle.Infrastructure/Repositories/ClubRepository.cs
  2. src/Koogle.Infrastructure/Repositories/PersonRepository.cs
  3. src/Koogle.Infrastructure/Repositories/ExpenseRepository.cs
  4. src/Koogle.Infrastructure/Repositories/DayRepository.cs
  5. src/Koogle.Infrastructure/Repositories/PersonExpenseRepository.cs

Pattern: IDbContextFactory, ClubId-Filter, Include Navigation Properties


Phase A3: DTOs erstellen

Dateien:

  1. src/Koogle.Application/DTOs/ClubDto.cs (ClubDto, CreateClubDto, UpdateClubDto)
  2. src/Koogle.Application/DTOs/PersonDto.cs (PersonDto, CreatePersonDto, UpdatePersonDto)
  3. src/Koogle.Application/DTOs/ExpenseDto.cs (ExpenseDto, CreateExpenseDto, UpdateExpenseDto)
  4. src/Koogle.Application/DTOs/DayDto.cs erweitern (DayDto, CreateDayDto, UpdateDayDto, DayParticipantDto)
  5. src/Koogle.Application/DTOs/PersonExpenseDto.cs (PersonExpenseDto, CreatePersonExpenseDto, DayEvaluationDto, PersonDayEvaluationDto)

Phase A4: Service Interfaces erstellen

Dateien:

  1. src/Koogle.Application/Interfaces/IClubService.cs
  2. src/Koogle.Application/Interfaces/IPersonService.cs
  3. src/Koogle.Application/Interfaces/IExpenseService.cs
  4. src/Koogle.Application/Interfaces/IDayService.cs (erweitern)
  5. src/Koogle.Application/Interfaces/IPersonExpenseService.cs

Methoden: GetAllAsync, GetByIdAsync, CreateAsync, UpdateAsync, DeleteAsync + spezifische Methoden


Phase A5: Service Implementations erstellen

Dateien:

  1. src/Koogle.Application/Services/ClubService.cs
  2. src/Koogle.Application/Services/PersonService.cs
  3. src/Koogle.Application/Services/ExpenseService.cs
  4. src/Koogle.Application/Services/DayService.cs (erweitern)
  5. src/Koogle.Application/Services/PersonExpenseService.cs

Dependencies: Repository, ICurrentClubContext, ICurrentUserService, IMapper Business Logic: ClubId-Injection, Audit-Felder, Validierung (mittels FluentValidation)


Phase A6: AutoMapper Profiles erstellen

Dateien:

  1. src/Koogle.Application/Mapping/ClubMappingProfile.cs
  2. src/Koogle.Application/Mapping/PersonMappingProfile.cs
  3. src/Koogle.Application/Mapping/ExpenseMappingProfile.cs
  4. src/Koogle.Application/Mapping/DayMappingProfile.cs
  5. src/Koogle.Application/Mapping/PersonExpenseMappingProfile.cs

Pattern: CreateMap<Entity, Dto>() bidirektional


Phase A7: DI Registration

Dateien ändern:

  1. src/Koogle.Infrastructure/DependencyInjection.cs (5 Repositories registrieren)
  2. src/Koogle.Application/DependencyInjection.cs (5 Services registrieren)

Phase B1: UserService erweitern

Dateien:

  1. src/Koogle.Application/Interfaces/IUserService.cs erweitern
  2. src/Koogle.Application/Services/UserService.cs erweitern
  3. src/Koogle.Application/DTOs/UserDto.cs erweitern (RegisterUserDto, ResetPasswordDto, UpdateUserProfileDto)

Neue Methoden:

  • RegisterUserAsync
  • RequestPasswordResetAsync
  • ResetPasswordAsync
  • UpdateProfileAsync
  • AssignUserToClubAsync, RemoveUserFromClubAsync
  • AssignClubRoleAsync, RemoveClubRoleAsync

Phase B2: Register Page

Dateien:

  1. src/Koogle.Web/Components/Pages/Account/Register.razor

Features: Email, Password, DisplayName, Optional ClubName für initiale Zuordnung


Phase B3: Password Reset Pages

Dateien:

  1. src/Koogle.Web/Components/Pages/Account/ForgotPassword.razor
  2. src/Koogle.Web/Components/Pages/Account/ResetPassword.razor

Flow: Email eingeben → Token per Email → Passwort zurücksetzen


Phase B4: Profile Page

Dateien:

  1. src/Koogle.Web/Components/Pages/Account/Profile.razor

Features: DisplayName, Locale, TimeZone, Club-Memberships anzeigen, Standard-Club setzen


Phase B5: Admin Users Page

Dateien:

  1. src/Koogle.Web/Components/Pages/Admin/Users.razor

Features: User-Liste, Club-Zuordnungen, Rollen pro Club


Phase C1: ClubState (Fluxor)

Dateien:

  1. src/Koogle.Web/Store/ClubState/ClubState.cs
  2. src/Koogle.Web/Store/ClubState/ClubActions.cs
  3. src/Koogle.Web/Store/ClubState/ClubReducers.cs
  4. src/Koogle.Web/Store/ClubState/ClubEffects.cs

Actions: Load, Create, Update, Delete


Phase C2: Club Pages

Dateien:

  1. src/Koogle.Web/Components/Pages/Admin/Clubs.razor

Features: MudTable, CRUD-Dialogs, SuperAdmin only


Phase D1: PersonState (Fluxor)

Dateien:

  1. src/Koogle.Web/Store/PersonState/PersonState.cs
  2. src/Koogle.Web/Store/PersonState/PersonActions.cs
  3. src/Koogle.Web/Store/PersonState/PersonReducers.cs
  4. src/Koogle.Web/Store/PersonState/PersonEffects.cs

Phase D2: Person Pages

Dateien:

  1. src/Koogle.Web/Components/Pages/Persons/Persons.razor

Features: MudTable mit Filter (Member/Guest), CRUD


Phase D3: ExpenseState (Fluxor)

Dateien:

  1. src/Koogle.Web/Store/ExpenseState/ExpenseState.cs
  2. src/Koogle.Web/Store/ExpenseState/ExpenseActions.cs
  3. src/Koogle.Web/Store/ExpenseState/ExpenseReducers.cs
  4. src/Koogle.Web/Store/ExpenseState/ExpenseEffects.cs

Phase D4: Expense Pages

Dateien:

  1. src/Koogle.Web/Components/Pages/Expenses/Expenses.razor

Features: MudTable mit Expense-Vorlagen, CRUD


Phase E1: DayState (Fluxor)

Dateien:

  1. src/Koogle.Web/Store/DayState/DayState.cs
  2. src/Koogle.Web/Store/DayState/DayActions.cs
  3. src/Koogle.Web/Store/DayState/DayReducers.cs
  4. src/Koogle.Web/Store/DayState/DayEffects.cs

State: Days, SelectedDay, SelectedDayExpenses, AvailablePersons


Phase E2: Days List Page

Dateien:

  1. src/Koogle.Web/Components/Pages/Days/Days.razor

Features: MudTable mit Jahr-Filter, Create Day


Phase E3: Day Details Page

Dateien:

  1. src/Koogle.Web/Components/Pages/Days/DayDetails.razor

Features:

  • Day-Header (Datum, Status)
  • Status-Workflow Buttons (New→Started→Closed)
  • Teilnehmer-Sektion (Add/Remove)
  • PersonExpense-Sektion

Phase E4: PersonExpense Management

Components in DayDetails:

  • PersonExpense-Tabelle
  • Add Expense Dialog (Person auswählen, Expense auswählen, Preis editierbar wenn IsVariable)
  • Delete PersonExpense (nur in New/Started)
  • IsInverse Logic: 1 Person auswählen → alle anderen bekommen Expense

Phase F1: Dashboard Page

Dateien:

  1. src/Koogle.Web/Components/Pages/Dashboard.razor

Features: Summary Cards, Recent Days, Top Penalty Recipients


Phase F2: Evaluation Components

Dateien:

  1. src/Koogle.Web/Components/Shared/DayEvaluationComponent.razor
  2. src/Koogle.Web/Components/Shared/PersonEvaluationComponent.razor

Phase F3: Navigation finalisieren

Dateien ändern:

  1. src/Koogle.Web/Components/Layout/NavMenu.razor

Features: Dashboard, Spieltage, Stammdaten, Admin, Profil


Phase G1: MembershipStatus + UserProfileClub erweitern

Dateien:

  1. src/Koogle.Domain/Enums/MembershipStatus.cs (neu)

    • Pending, Approved, Rejected
  2. src/Koogle.Domain/Entities/UserProfileClub.cs (erweitern)

    • MembershipStatus Status (default: Pending)
    • string? RejectionReason
    • DateTime? ApprovedAt, Guid? ApprovedById
    • DateTime? RejectedAt, Guid? RejectedById
  3. Migration erstellen


Phase G2: ClubInvitation Entity

Dateien:

  1. src/Koogle.Domain/Entities/ClubInvitation.cs (neu)

    • Guid Id, Guid ClubId
    • string Token (unique, für URL)
    • DateTime ExpiresAt, DateTime CreatedAt, Guid CreatedById
    • int? MaxUses (null = unbegrenzt), int UsedCount
  2. src/Koogle.Infrastructure/Data/AppDbContext.cs - DbSet hinzufügen

  3. Migration erstellen


Phase G3: IEmailService (Stub)

Dateien:

  1. src/Koogle.Application/Interfaces/IEmailService.cs (neu)

    • SendMembershipRequestNotificationAsync
    • SendMembershipApprovedAsync
    • SendMembershipRejectedAsync
  2. src/Koogle.Infrastructure/Services/StubEmailService.cs (neu)

    • Logging statt echtem Versand
    • TODO-Kommentare für SMTP
  3. DI Registration


Phase G4: Services erweitern (Membership-Logik)

UserService erweitern:

  • RequestClubMembershipAsync(userProfileId, clubId)
  • RequestClubMembershipByNameAsync(userProfileId, clubName)
  • RequestClubMembershipByInviteAsync(userProfileId, inviteToken)
  • ApproveMembershipAsync(userProfileId, clubId, approvedById)
  • RejectMembershipAsync(userProfileId, clubId, rejectedById, reason)
  • GetPendingMembershipsAsync(clubId)

ClubService erweitern:

  • CreateInvitationAsync(clubId, createdById, expiresAt, maxUses)
  • GetInvitationByTokenAsync(token)
  • ValidateInvitationAsync(token)

Phase G5: Dashboard Pending-Widget

Dateien:

  1. src/Koogle.Web/Components/Shared/PendingMembershipsWidget.razor (neu)

    • Anzahl ausstehender Anträge für Club-Admins
    • Link zur Admin Users Page
  2. Dashboard.razor erweitern - Widget für Admins einbinden


Phase G6: Admin Users Page erweitern

Dateien:

  1. src/Koogle.Web/Components/Pages/Admin/Users.razor (ändern)
    • Tab/Filter für "Ausstehende Anträge"
    • Approve/Reject Buttons
    • Reject-Dialog mit Begründungsfeld

Phase G7: Club-Beitritt UI

Dateien:

  1. src/Koogle.Web/Components/Pages/Account/JoinClub.razor (neu)

    • Eingabefeld für Club-Name
    • Suche/Validierung
    • Beitrittsantrag senden
    • Erfolgsmeldung "Antrag gestellt"
  2. Dashboard.razor erweitern

    • "Keinem Club zugeordnet" Meldung mit Link zu JoinClub

Dateien:

  1. src/Koogle.Web/Components/Pages/Club/JoinByInvite.razor (neu)

    • Route: /club/join/{token}
    • Token validieren
    • Wenn eingeloggt: direkt Beitritt (Pending)
    • Wenn nicht eingeloggt: Redirect zu Register mit Token
  2. src/Koogle.Web/Controllers/AuthController.cs (erweitern)

    • InviteToken als optionalen Parameter bei Register

Zentrale Anforderungen (User-Feedback)

Business Rules

  1. PersonExpense.Price: Bei IsVariable=true editierbar, aber vorbelegt aus Expense
  2. Day Status-Workflow: Strikt New→Started→Closed, keine Sprünge
  3. User-Registrierung: Self-Service (öffentlich)
  4. Passwort-Reset: Email-basiert mit Token
  5. Day-Create: Alle Members vorbelegt, Gäste manuell
  6. PersonExpense löschen: Nur in Status New/Started
  7. Expense.IsInverse: Automatisch - 1 Person auswählen, alle anderen bekommen Expense
  8. Dashboard Zeitraum: Aktuelles Jahr

Code-Patterns & Konventionen

Repository Pattern

  • Interface in Domain/Interfaces
  • Implementation in Infrastructure/Repositories
  • Standard-Methoden: GetAllAsync, GetByIdAsync, AddAsync, UpdateAsync, DeleteAsync
  • ClubId-Filter via ICurrentClubContext
  • IDbContextFactory für Scoping

Service Pattern

  • Interface in Application/Interfaces
  • Implementation in Application/Services
  • Dependencies: Repository, ICurrentClubContext, ICurrentUserService, IMapper
  • ClubService: IsSuperAdmin Check
  • Audit-Felder auto-setzen (CreatedById, CreatedAt, ModifiedById, ModifiedAt)

Fluxor Pattern (Redux)

  • State: Record mit Collections + IsLoading + Error
  • Actions: Record per Operation (Load, LoadSuccess, LoadFailure, Create, Update, Delete)
  • Reducers: Pure functions, State transformieren
  • Effects: Async Operations, Service-Calls, Dispatcher

UI Pattern (Blazor + MudBlazor)

  • @inherits FluxorComponent
  • @attribute [Authorize(Policy = "...")]
  • MudTable für Listen
  • MudDialog für Create/Edit
  • MudForm mit Validation

Referenzdateien für Patterns

Service-Pattern:

  • src/Koogle.Application/Services/UserService.cs

Fluxor-Pattern:

  • src/Koogle.Web/Store/AuthState/AuthState.cs
  • src/Koogle.Web/Store/AuthState/AuthActions.cs
  • src/Koogle.Web/Store/AuthState/AuthReducers.cs
  • src/Koogle.Web/Store/AuthState/AuthEffects.cs

AutoMapper:

  • src/Koogle.Application/Mapping/UserProfile.cs

DI:

  • src/Koogle.Infrastructure/DependencyInjection.cs
  • src/Koogle.Application/DependencyInjection.cs

Domain:

  • src/Koogle.Domain/Entities/BaseEntity.cs

Berechtigungen pro Feature

  • Club-Verwaltung: SuperAdmin only
  • Person/Expense/Day CRUD: ClubEditor+
  • Dashboard/Auswertungen: ClubViewer+

Policy: @attribute [Authorize(Policy = "ClubViewer|ClubEditor|ClubAdmin")]


Zusammenfassung Phase 1

23 feine Phasen~75 DateienMVP Phase 1 komplett



IMPLEMENTIERUNGSPLAN - Phase 2: Detaillierte Spielverwaltung

Übersicht

Implementierung des Game-Management-Systems in Koogle.Web mit Clean Architecture. Code wird komplett neu geschrieben, KoogleApp dient nur als Referenz. Spiele laufen im Speicher auf der DayDetails-Seite ohne Browser-Reload.


Geklärte Anforderungen

Aspekt Entscheidung
Spiel-Ende Auto-Ende + manuelle "Beenden" Option
Strafen-Schnellwahl Nur im Details-Tab, nicht in Eingabe/Tafel
Multi-Game Sequentiell - mehrere Spiele pro Tag, Liste sichtbar
Persistenz Nach jedem Wurf in DB speichern (Game.GameData)
Multi-User Spielfortschritt in anderen Sessions via SignalR sichtbar
Scheiss-Spiel Ende Erster mit 0 Punkten gewinnt, andere bekommen Strafen nach Restpunkten
Strafen-Berechnung Trigger ExpenseTriggerType.ExpensePoint mit Multiplikator = Restpunkte
SignalR Komplett neu aufsetzen + Auto-Reconnect
Port-Strategie Komplett neu schreiben, KoogleApp nur als Referenz
Concurrency RowVersion auf Game Entity für Optimistic Concurrency
Undo-Tiefe Unbegrenzt (komplette Spiel-Historie)
Testing Nur Kern-Logik (GameLogicService)

Design-Entscheidungen

Entscheidung Lösung
State-Speicherung Separater GameState (nicht in DayState)
View-Wechsel ViewMode-Enum + MudTabs (kein URL-Routing)
Spieltyp-System IGameDefinition in Domain, Registry in Application
Pin-Komponenten Neu schreiben (nicht portieren)
Persistenz Nach jedem Wurf → DB (Game.GameData JSON)
Recovery Bei Page-Load: aktives Spiel aus DB laden
Multi-User Sync SignalR mit Auto-Reconnect für Echtzeit-Updates
Strafen-Engine ITriggerService für automatische Strafen

Game Entity (erweitern)

// Koogle.Domain/Entities/Game.cs - erweitern
public class Game : BaseEntity
{
    public string GameData { get; set; } = "{}";  // JSON für kompletten Spielzustand
    public Guid DayId { get; set; }
    public Day Day { get; set; }
    public ICollection<GamePerson> GamePersons { get; set; }
    public Guid ClubId { get; set; }
    public Club Club { get; set; }

    // NEU:
    public string GameType { get; set; } = "";     // "Training", "Shit"
    public GameStatus Status { get; set; }         // Active, Completed, Aborted
    public DateTime? StartedAt { get; set; }
    public DateTime? CompletedAt { get; set; }
    public byte[] RowVersion { get; set; }         // Optimistic Concurrency
}

public enum GameStatus { Active, Completed, Aborted }

GameData JSON Struktur (Beispiel Training)

{
  "gameType": "Training",
  "throwPanelState": {
    "throwsPerRound": 3,
    "throwMode": "Reposition",
    "totalThrowCounter": 42
  },
  "participants": {
    "playerIds": ["guid1", "guid2"],
    "currentPlayerIndex": 1
  },
  "gameModel": {
    "playerStatistics": {
      "guid1": { "throwCount": 21, "pinCount": 145, "circleCount": 2 },
      "guid2": { "throwCount": 21, "pinCount": 138, "circleCount": 1 }
    }
  },
  "undoStack": [ /* Unbegrenzte Historie */ ]
}

Architektur

Domain:     GameType Enum, GameStatus Enum, IGameDefinition Interface
Application: Games/ mit GameProgress, IGameLogicService, ITriggerService, Training/, Shit/
Infrastructure: TriggerRepository, GameRepository
Web:        Store/GameState/, Components/Game/, Hubs/GameHub

Umsetzungsreihenfolge Phase 2

Phase Bereich Beschreibung Dateien
H0 Trigger-Engine ITriggerService + Repository 5
H1 Foundation GameState Fluxor + Enums 6
H2 Components Pin Input Components (neu schreiben) 5
H3 Framework Game Definition Framework 5
H4 Games Training Game 5
H5 Games Scheiss-Spiel + Trigger-Integration 6
H6 UI Game Setup Dialog 4
H7 Integration DayDetails Tabs 3
H8 Features Undo (unbegrenzt, ohne Redo) 2
H9 Persistenz DB Persistence & Recovery 4
H9b Sync SignalR komplett + Auto-Reconnect 5
H10 Testing Kern-Logik Tests 2

Geschätzte Dateien: ~52 neue/modifizierte


Detaillierte Phasen

Phase H0: Trigger-Engine (NEU)

Die Trigger-Engine wird für automatische Strafen benötigt (Scheiss-Spiel ist nur ein Beispiel, alle Spiele lösen Trigger für Strafen aus).

Dateien:

  1. src/Koogle.Domain/Interfaces/ITriggerRepository.cs
  2. src/Koogle.Infrastructure/Repositories/TriggerRepository.cs
  3. src/Koogle.Application/Interfaces/ITriggerService.cs
  4. src/Koogle.Application/Services/TriggerService.cs
  5. src/Koogle.Application/DTOs/TriggerDto.cs

ITriggerService:

public interface ITriggerService
{
    /// <summary>
    /// Gets all expenses linked to a trigger type for the current club.
    /// </summary>
    Task<IEnumerable<ExpenseDto>> GetExpensesForTriggerAsync(ExpenseTriggerType type);

    /// <summary>
    /// Fires a trigger, creating PersonExpenses for linked expenses.
    /// </summary>
    /// <param name="type">The trigger type (e.g., ExpensePoint)</param>
    /// <param name="personId">Person receiving the expense</param>
    /// <param name="dayId">Day context</param>
    /// <param name="gameId">Optional game context</param>
    /// <param name="multiplier">Multiplier for expense price (e.g., remaining points)</param>
    Task FireTriggerAsync(
        ExpenseTriggerType type,
        Guid personId,
        Guid dayId,
        Guid? gameId = null,
        int multiplier = 1);
}

Logik FireTriggerAsync:

  1. Hole alle Expenses die mit Trigger-Typ verknüpft sind (via ExpenseTrigger Join-Table)
  2. Für jede Expense: Erstelle PersonExpense mit Price = Expense.Price × multiplier
  3. multiplier = Restpunkte, z.B. im Scheiss-Spiel

Phase H1: GameState Foundation

Dateien:

  1. src/Koogle.Web/Store/GameState/GameState.cs
  2. src/Koogle.Web/Store/GameState/GameActions.cs
  3. src/Koogle.Web/Store/GameState/GameReducers.cs
  4. src/Koogle.Web/Store/GameState/GameEffects.cs
  5. src/Koogle.Domain/Enums/GameType.cs
  6. src/Koogle.Domain/Enums/PinStatus.cs

State-Struktur:

[FeatureState]
public record GameState
{
    public bool IsGameActive { get; init; }
    public string? GameTypeName { get; init; } // "Training", "Shit"
    public Guid? DayId { get; init; }
    public Guid? ActiveGameId { get; init; }
    public ThrowPanelState ThrowPanel { get; init; }
    public ParticipantsState Participants { get; init; }
    public object? GameModel { get; init; } // JSON-serializable game-specific data
    public ImmutableList<GameSnapshot> UndoStack { get; init; } // Unbegrenzt
    public IReadOnlyList<GameSummaryDto> CompletedGames { get; init; }
    public bool IsLoading { get; init; }
    public string? Error { get; init; }
    public bool IsConcurrencyConflict { get; init; } // RowVersion conflict
}

public record ThrowPanelState(
    bool IsStarted,
    PinStatus Pin1, PinStatus Pin2, PinStatus Pin3,
    PinStatus Pin4, PinStatus Pin5, PinStatus Pin6,
    PinStatus Pin7, PinStatus Pin8, PinStatus Pin9,
    int ThrowsPerRound,
    int ThrowCounterPerRound,
    ThrowMode ThrowMode,
    int TotalThrowCounter,
    bool BellValue
);

public record ParticipantsState(
    Guid[] PlayerIds,
    int CurrentPlayerIndex,
    ParticipantsMode Mode
);

public record GameSnapshot(
    ThrowPanelState ThrowPanel,
    ParticipantsState Participants,
    object GameModel
);

Phase H2: Pin Input Components

Dateien (NEU SCHREIBEN):

  1. src/Koogle.Web/Components/Game/Pin.razor
  2. src/Koogle.Web/Components/Game/PinPanel.razor
  3. src/Koogle.Web/Components/Game/NumberPanel.razor
  4. src/Koogle.Web/Components/Game/ThrowPanel.razor
  5. src/Koogle.Web/Components/Game/GameInputPanel.razor

Pin-Layout (9 Kegel):

     ①
   ②   ③
  ④  ⑤  ⑥
   ⑦   ⑧
     ⑨

Features:

  • Pin.razor: Einzelner Kegel, klickbar, Status (Standing/Fallen/Disabled)
  • PinPanel.razor: 9-Pin-Layout, Touch-optimiert
  • NumberPanel.razor: Schnelleingabe 1-9, Wurf-Button, Glocke
  • ThrowPanel.razor: Wurf-Bestätigung, Rinnen links/rechts
  • GameInputPanel.razor: Orchestriert alles, zeigt aktuellen Spieler

Phase H3: Game Definition Framework

Dateien:

  1. src/Koogle.Domain/Interfaces/IGameDefinition.cs
  2. src/Koogle.Application/Games/GameProgress.cs
  3. src/Koogle.Application/Games/IGameLogicService.cs
  4. src/Koogle.Application/Games/GameDefinitionRegistry.cs
  5. src/Koogle.Application/DependencyInjection.cs (erweitern)

IGameDefinition:

public interface IGameDefinition
{
    string Name { get; }           // "Training"
    string DisplayName { get; }    // "Kegel-Training"
    Type SetupComponentType { get; }
    Type BoardComponentType { get; }
    Type GameLogicServiceType { get; }
    Type GameModelType { get; }
}

GameProgress:

  • BeforeThrowState, AfterThrowState Records
  • PinCount(), IsCircle() Extension Methods
  • NextRound(), EndOfGame() Helpers

GameModelFactory (für Polymorphie):

public static class GameModelFactory
{
    public static object Deserialize(string json, string gameType)
    {
        return gameType switch
        {
            "Training" => JsonSerializer.Deserialize<TrainingGameModel>(json),
            "Shit" => JsonSerializer.Deserialize<ShitGameModel>(json),
            _ => throw new NotSupportedException($"Unknown game type: {gameType}")
        };
    }
}

Phase H4: Training Game

Dateien:

  1. src/Koogle.Application/Games/Training/TrainingGameDefinition.cs
  2. src/Koogle.Application/Games/Training/TrainingGameModel.cs
  3. src/Koogle.Application/Games/Training/TrainingGameLogicService.cs
  4. src/Koogle.Web/Components/Game/Training/TrainingSetup.razor
  5. src/Koogle.Web/Components/Game/Training/TrainingBoard.razor

TrainingGameModel:

public record TrainingGameModel
{
    public Dictionary<Guid, PlayerStats> PlayerStatistics { get; init; }
}

public record PlayerStats
{
    public int ThrowCount { get; set; }
    public int PinCount { get; set; }
    public int CircleCount { get; set; }  // Kränze (8+1)
    public int StrikeCount { get; set; }  // Alle 9
}

TrainingSetup.razor:

  • ThrowMode: Reposition (in die Vollen) / Decrease (Abräumen)
  • ThrowsPerRound: 1-5
  • ParticipantsMode: GameLogic (Rotation) / FreeToChoose

TrainingBoard.razor (Tafel):

Spieler Würfe Kegel Kränze
Max 24 156 3 6.5

Phase H5: Scheiss-Spiel + Trigger-Integration

Dateien:

  1. src/Koogle.Application/Games/Shit/ShitGameDefinition.cs
  2. src/Koogle.Application/Games/Shit/ShitGameModel.cs
  3. src/Koogle.Application/Games/Shit/ShitGameLogicService.cs
  4. src/Koogle.Web/Components/Game/Shit/ShitSetup.razor
  5. src/Koogle.Web/Components/Game/Shit/ShitBoard.razor
  6. Integration mit ITriggerService

ShitGameModel:

public record ShitGameModel
{
    public int ShitNumber { get; init; }        // 1-9 (Scheiss-Zahl)
    public int StartNumber { get; init; }       // 10-1000 (Startzahl)
    public Dictionary<Guid, int> PlayerPoints { get; init; }
    public int CollectedPoints { get; set; }
    public Guid? WinnerId { get; set; }
}

Spiellogik:

  1. Spieler wirft
  2. Wenn Scheiss-Zahl oder Rinne → bis dahin gesammelte Punkte werden zum Konto addiert, nächster Spieler
  3. Sonst → Punkte werden gesammelt und subtrahiert, denn der Spieler entscheidet die Punkte mitzunehmenan den nächsten Spieler zu übergeben (subtrahieren). Spiel-Spezifischer "Weiter-Button" wird benötigt.
  4. Erster mit 0 Punkten → GEWINNER
  5. Spiel endet, für ein Spieler 0 erreicht hat. Alle danderen sind Verlierer: ITriggerService.FireTriggerAsync(ExpensePoint, personId, dayId, gameId, restpunkte)

ShitSetup.razor:

  • Scheiss-Zahl (1-9, Default: 5)
  • Start-Zahl (10-1000, Default: 50)
  • Warnung wenn kein ExpensePoint-Trigger konfiguriert

ShitBoard.razor (Tafel):

Spieler Punkte Status
Max 23 🎯
Anna 0 🏆 GEWINNER

Phase H6: Game Setup Dialog

Dateien:

  1. src/Koogle.Web/Components/Game/GameSetupDialog.razor
  2. src/Koogle.Web/Components/Game/GameTypeSelector.razor
  3. src/Koogle.Web/Components/Game/ParticipantSelector.razor
  4. src/Koogle.Web/Components/Game/CommonSetupOptions.razor

Flow:

  1. Dialog öffnen
  2. Spieltyp auswählen (Training, Scheiss-Spiel)
  3. DynamicComponent rendert spieltyp-spezifisches Setup
  4. Teilnehmer aus Day-Participants auswählen
  5. "Spiel starten" → StartGameAction dispatch

Phase H7: DayDetails Integration

Dateien ändern:

  1. src/Koogle.Web/Components/Pages/Days/DayDetails.razor
  2. src/Koogle.Web/Components/Game/GameBoardPanel.razor
  3. src/Koogle.Web/Components/Game/CompletedGamesList.razor

DayDetails Erweiterung:

@code {
    private int _activeTabIndex = 0;
}

<MudTabs @bind-ActivePanelIndex="_activeTabIndex">
    <MudTabPanel Text="Details" Icon="@Icons.Material.Filled.Info">
        <!-- Bestehend: Participants + Expenses + Quick-Assign -->
    </MudTabPanel>

    <MudTabPanel Text="Eingabe" Icon="@Icons.Material.Filled.SportsScore"
                 Disabled="@(!GameState.Value.IsGameActive)">
        <GameInputPanel />
    </MudTabPanel>

    <MudTabPanel Text="Tafel" Icon="@Icons.Material.Filled.TableChart"
                 Disabled="@(!GameState.Value.IsGameActive)">
        <GameBoardPanel />
    </MudTabPanel>
</MudTabs>

<!-- Spiel starten Button (nur wenn Day.Status == Started und kein aktives Spiel) -->
@if (SelectedDay?.Status == DayStatus.Started && !GameState.Value.IsGameActive)
{
    <MudButton OnClick="OpenGameSetupDialog" Color="Color.Primary">
        Neues Spiel starten
    </MudButton>
}

<!-- Liste abgeschlossener Spiele -->
<CompletedGamesList DayId="DayId" />

Phase H8: Undo (Unbegrenzt)

Dateien:

  1. src/Koogle.Web/Store/GameState/GameState.cs (UndoStack bereits definiert)
  2. src/Koogle.Web/Components/Game/UndoButton.razor

Logic:

  • Jeder Wurf erstellt GameSnapshot → auf UndoStack pushen
  • Stack ist unbegrenzt (komplette Spiel-Historie)
  • "Letzten Wurf rückgängig" Button
  • Bei Undo: Pop vom Stack, State wiederherstellen

Phase H9: DB Persistence & Recovery

Dateien:

  1. src/Koogle.Application/Interfaces/IGamePersistenceService.cs
  2. src/Koogle.Application/Services/GamePersistenceService.cs
  3. src/Koogle.Web/Store/GameState/GameEffects.cs (erweitern)
  4. src/Koogle.Application/DTOs/GameStateDto.cs (JSON-Serialisierung)

Methoden:

  • SaveGameStateAsync(Guid gameId, GameState state) - Nach jedem Wurf
  • LoadActiveGameAsync(Guid dayId) - Bei Page-Load
  • GetCompletedGamesAsync(Guid dayId) - Für Spiele-Liste

Persistenz-Flow:

1. Spiel starten → Game Entity mit Status=Active erstellen
2. Nach jedem Wurf:
   - GameState → JSON serialisieren (inkl. UndoStack)
   - Game.GameData updaten (Debounced, max 1x/500ms)
   - RowVersion prüfen → bei Konflikt: IsConcurrencyConflict = true
3. Page Reload / Andere Session:
   - LoadActiveGameAsync(dayId)
   - Wenn Active Game existiert → GameState wiederherstellen
4. Spiel beenden:
   - Status = Completed, CompletedAt = now
   - Finales GameData speichern

Concurrency-Handling:

try
{
    await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
    // Reload aktuellen State, UI informieren
    dispatcher.Dispatch(new ConcurrencyConflictAction(gameId));
}

Phase H9b: SignalR Live-Updates (Komplett)

Dateien:

  1. src/Koogle.Web/Hubs/GameHub.cs
  2. src/Koogle.Web/Hubs/IGameHubClient.cs
  3. src/Koogle.Web/Services/GameHubService.cs (Wrapper)
  4. src/Koogle.Web/Store/GameState/GameEffects.cs (erweitern für Broadcast)
  5. src/Koogle.Web/Program.cs (AddSignalR, MapHub)

Program.cs Erweiterung:

// Services
builder.Services.AddSignalR();
builder.Services.AddScoped<GameHubService>();

// Endpoints
app.MapHub<GameHub>("/gamehub");

SignalR Hub:

public class GameHub : Hub<IGameHubClient>
{
    public async Task JoinGame(Guid gameId)
        => await Groups.AddToGroupAsync(Context.ConnectionId, gameId.ToString());

    public async Task LeaveGame(Guid gameId)
        => await Groups.RemoveFromGroupAsync(Context.ConnectionId, gameId.ToString());
}

public interface IGameHubClient
{
    Task GameStateUpdated(GameStateDto state);
    Task GameEnded(GameResultDto result);
    Task ConcurrencyConflict(Guid gameId);
}

Auto-Reconnect (Client-Side):

_hubConnection = new HubConnectionBuilder()
    .WithUrl(Navigation.ToAbsoluteUri("/gamehub"))
    .WithAutomaticReconnect(new[] {
        TimeSpan.Zero,
        TimeSpan.FromSeconds(2),
        TimeSpan.FromSeconds(10),
        TimeSpan.FromSeconds(30)
    })
    .Build();

_hubConnection.Reconnected += async (connectionId) =>
{
    // Re-join active game
    if (GameState.Value.ActiveGameId.HasValue)
    {
        await _hubConnection.InvokeAsync("JoinGame", GameState.Value.ActiveGameId.Value);
        // Reload current state from DB
        Dispatcher.Dispatch(new LoadActiveGameAction(GameState.Value.DayId!.Value));
    }
};

Flow:

  1. Tafel-View öffnen → JoinGame(gameId)
  2. Nach jedem Wurf: DB Save → Clients.Group(gameId).GameStateUpdated(state)
  3. Andere Sessions empfangen Update → Reducer aktualisiert State
  4. Bei Reconnect → State aus DB laden
  5. Tafel-View schließen → LeaveGame(gameId)

Phase H10: Testing (Kern-Logik)

Dateien:

  1. test/Koogle.Tests/Application/Games/TrainingGameLogicServiceTests.cs
  2. test/Koogle.Tests/Application/Games/ShitGameLogicServiceTests.cs

Test-Cases:

  • Pin-Count Berechnung (0-9 Kegel)
  • Kranz-Erkennung (8 äußere + Mitte steht = 8 Punkte)
  • Scheiss-Spiel: Gewinner bei 0 Punkten
  • Scheiss-Spiel: Korrekte Punkte-Abzüge
  • Training: Statistik-Updates nach Wurf

Migration

# Game Entity erweitern: GameType, Status, StartedAt, CompletedAt, RowVersion
dotnet ef migrations add ExtendGameEntity --project src/Koogle.Infrastructure --startup-project src/Koogle.Web --context AppDbContext --output-dir Data/Migrations

dotnet ef database update -p src/Koogle.Infrastructure -s src/Koogle.Web --context AppDbContext

GameConfiguration.cs erweitern:

builder.Property(g => g.RowVersion)
    .IsRowVersion()
    .IsConcurrencyToken();

Berechtigungen Phase 2

  • Spiel starten/beenden: ClubEditor+
  • Würfe eingeben: ClubEditor+
  • Tafel ansehen: ClubViewer+

Risiken & Mitigationen

Risiko Mitigation
Polymorphe GameModel-Serialisierung GameModelFactory mit switch über GameType
SignalR + Blazor Server Separater Hub /gamehub, keine Interferenz
Concurrent Updates RowVersion + UI-Konfliktwarnung
Fehlende Trigger-Stammdaten Warnung in ShitSetup wenn kein ExpensePoint-Trigger

Zusammenfassung Phase 2

12 Phasen (H0-H10 inkl. H9b)~52 DateienDetaillierte Spielverwaltung

Kernfeatures:

  • Spiele mit Wurf-für-Wurf Eingabe
  • Trigger-Engine für automatische Strafen
  • DB-Persistenz nach jedem Wurf mit RowVersion
  • SignalR Live-Updates + Auto-Reconnect für Multi-User
  • Recovery bei Page-Reload
  • Unbegrenzte Undo-Historie

Abhängigkeiten zu bestehenden Entities:

  • ExpenseTrigger + Trigger (bereits vorhanden)
  • ExpenseTriggerType.ExpensePoint (bereits vorhanden)
  • PersonExpense mit GameId (bereits vorhanden)

Bereit für Implementierung Phase H0


IMPLEMENTIERUNGSPLAN - Phase 3: Stammdaten-UI

Umsetzungsreihenfolge Phase 3

Phase Bereich Beschreibung Dateien
I1 Stammdaten Trigger-Expense Config UI 6

Detaillierte Phasen

Phase I1: Trigger-Expense Konfiguration UI

UI-Seite zur clubspezifischen Konfiguration der Trigger-Expense-Zuordnungen.

Route: /expensetriggers Policy: ClubEditor

Dateien:

  1. src/Koogle.Web/Store/TriggerState/TriggerState.cs - Fluxor State
  2. src/Koogle.Web/Store/TriggerState/TriggerActions.cs - Actions
  3. src/Koogle.Web/Store/TriggerState/TriggerReducers.cs - Reducers
  4. src/Koogle.Web/Store/TriggerState/TriggerEffects.cs - Effects
  5. src/Koogle.Web/Components/Pages/Expenses/ExpenseTriggerConfig.razor - UI
  6. src/Koogle.Web/Components/Pages/Expenses/TriggerEditDialog.razor - Edit Dialog
  7. src/Koogle.Web/Components/Layout/NavMenu.razor - Nav-Link (erweitert)

Features:

  • MudTable mit allen 10 ExpenseTriggerType-Werten
  • Inline-Dropdown zum Verknuepfen von Expenses
  • MudChips fuer verknuepfte Expenses mit Close-Button
  • Confirm-Dialog vor Entfernen einer Verknuepfung
  • Edit-Dialog zum Bearbeiten von Name, Beschreibung und Typ

Migration:

  • AddTriggerDescription - Trigger.Description Feld hinzugefuegt

Zusammenfassung Phase 3

1 Phase (I1)6 DateienTrigger-Stammdaten UI



IMPLEMENTIERUNGSPLAN - Phase 4: DeathBox (Totenkisten)

Übersicht

DeathBox ist ein Eliminationsspiel nach dem Hangman-Prinzip. Spieler sammeln Striche (Marks) und scheiden aus wenn ihr Sarg voll ist. Der letzte überlebende Spieler gewinnt.

Backend: Vollständig implementiert (GameLogic, Model, Setup, Definition)


Umsetzungsreihenfolge Phase 4

Phase Bereich Beschreibung Dateien
J1 DeathBox UI-Komponenten 3
J2 DeathBox Unit Tests 1

Detaillierte Phasen

Phase J1: DeathBox UI

Dateien:

  1. src/Koogle.Web/Components/Game/DeathBox/DeathBoxSetup.razor (neu)
  2. src/Koogle.Web/Components/Game/DeathBox/DeathBoxBoard.razor (neu)
  3. src/Koogle.Web/Program.cs (erweitern - Registration)

Features:

  • Setup: CoffinSize (6-12), RandomizePlayerOrder
  • Board: Sarg-Fortschritt (MudProgressLinear), Xs (✗), Eier (🥚), LastThrow-Info
  • Warnung wenn Trigger nicht konfiguriert

Phase J2: DeathBox Tests

Dateien:

  1. test/Koogle.Tests/Application/Games/DeathBoxGameLogicServiceTests.cs

Test-Cases:

  • Neue Runde sammelt X
  • Neue Runde mit <3 Pins → Strafmark
  • 3 Xs → Mark-Konvertierung
  • Cleared → Ei für current, Mark für previous
  • 3 Eier → Mark-Entfernung
  • Elimination + ExpensePoint Trigger
  • Gewinner-Ermittlung

Spielregeln (Referenz)

  • Sarggröße 6-12 Striche, zufällige Spielerreihenfolge
  • Modus: Abräumen (ThrowMode.Decrease), 1 Wurf pro Spieler
  • Neue Runde (9 Pins): +1 X sammeln, bei <3 Pins zusätzlich +1 Strich
  • Gosse/kein Holz: +1 Strich
  • Abräumen: +1 Ei (aktueller Spieler), +1 Strich (vorheriger Spieler)
  • 3 Xe → +1 Strich, 3 Eier → -1 Strich (wenn möglich)
  • Ausscheiden bei Sarg voll → ExpensePoint × verbleibende Spieler
  • Letzter Spieler gewinnt

Zusammenfassung Phase 4

2 Phasen (J1-J2)4 DateienDeathBox spielbar



IMPLEMENTIERUNGSPLAN - Phase 5: Kassenbuch (Cash Book)

Übersicht

Kassenbuch-Modul für Vereinsfinanzverwaltung. Trackt Einnahmen/Ausgaben, integriert mit Day-Close für Strafen-Buchungen, unterstützt Mitgliedsbeiträge und bietet Berichte mit Excel/PDF-Export.


Geklärte Anforderungen

Aspekt Entscheidung
Rolle Neue Rolle "Kassenwart" (separate von Editor/Admin)
Penalty-Buchung Eine Buchung pro Person/Tag (aggregierte Summe)
Kontostand Eröffnungssaldo in Club-Settings speichern
Export ClosedXML (Excel) + QuestPDF (PDF)
Kategorien Entity mit IsSystemCategory-Flag
Mitgliedsbeiträge Monat wählbar, Warnung bei Duplikaten
PersonExpenses Automatisch Done bei CashBookEntry-Erstellung
Korrekturbuchung Eine Kategorie für Einnahme + Ausgabe

Neue Entities

BookingCategory Entity

// src/Koogle.Domain/Entities/BookingCategory.cs
public class BookingCategory : BaseEntity
{
    public Guid ClubId { get; set; }
    public string Name { get; set; } = string.Empty;
    public string? Description { get; set; }
    public BookingCategoryType CategoryType { get; set; } // Income/Expense
    public bool IsSystemCategory { get; set; } // Nicht löschbar
    public string? Color { get; set; } // Hex-Farbcode
    public string? Icon { get; set; } // MudBlazor Icon-Name
    public bool IsActive { get; set; } = true; // Soft-Deaktivierung

    public Club Club { get; set; } = null!;
    public ICollection<CashBookEntry> CashBookEntries { get; set; } = [];
}

CashBookEntry Entity

// src/Koogle.Domain/Entities/CashBookEntry.cs
public class CashBookEntry : BaseEntity
{
    public Guid ClubId { get; set; }
    public Guid CategoryId { get; set; }
    public CashBookEntryType EntryType { get; set; } // Income/Expense
    public decimal Amount { get; set; } // Immer positiv
    public DateTime BookingDate { get; set; } // Frei wählbar
    public string? Comment { get; set; }
    public string? ReceiptReference { get; set; } // Belegverweis
    public Guid? DayId { get; set; } // Link zu Spieltag (Strafen)
    public Guid? PersonId { get; set; } // Link zu Person

    public Club Club { get; set; } = null!;
    public BookingCategory Category { get; set; } = null!;
    public Day? Day { get; set; }
    public Person? Person { get; set; }
}

Club Entity Erweiterung

// Neue Properties in Club.cs
public decimal InitialBalance { get; set; } // Eröffnungssaldo
public decimal MonthlyMembershipFee { get; set; } // Monatsbeitrag

Neue Enums

// src/Koogle.Domain/Enums/BookingCategoryType.cs
public enum BookingCategoryType { Income = 0, Expense = 1 }

// src/Koogle.Domain/Enums/CashBookEntryType.cs
public enum CashBookEntryType { Income = 0, Expense = 1 }

System-Kategorien (pro Club geseeded)

Name Typ Farbe Beschreibung
Spielstrafe Income Green Strafen aus Day-Close
Mitgliedsbeitrag Income Blue Monatliche Beiträge
Korrekturbuchung Both Orange Manuelle Korrekturen
Saldoanpassung Both Gray Kontoabgleich

Umsetzungsreihenfolge Phase 5

Phase Bereich Beschreibung Dateien
K1 Domain Entities + Enums 5
K2 Infrastructure EF Configurations 4
K3 Infrastructure Migration -
K4 Security Kassenwart Role + Policy 4
K5 Infrastructure Repositories 5
K6 Application DTOs 3
K7 Application Service Interfaces 2
K8 Application Service Implementations 4
K9 Application Category Seeder 1
K10 Application Day Close Integration 1
K11 Web Fluxor CategoryState 4
K12 Web Fluxor CashBookState 4
K13 Web CashBook UI 3
K14 Web Categories UI 2
K15 Web Reports UI 2
K16 Application Excel Export (ClosedXML) 3
K17 Application PDF Export (QuestPDF) 1
K18 Web Export Controller 1
K19 Web Membership Fees Feature 2
K20 Web Club Settings Extension 3
K21 Web Navigation Integration 1
K22 Tests Unit Tests 2
K23 Tests Integration Tests 1

Legende: ☐ = Offen | ☑ = In Arbeit | ✓ = Fertig


Detaillierte Phasen

Phase K1: Domain Layer

Dateien:

  1. src/Koogle.Domain/Enums/BookingCategoryType.cs (neu)
  2. src/Koogle.Domain/Enums/CashBookEntryType.cs (neu)
  3. src/Koogle.Domain/Entities/BookingCategory.cs (neu)
  4. src/Koogle.Domain/Entities/CashBookEntry.cs (neu)
  5. src/Koogle.Domain/Entities/Club.cs (erweitern)

Phase K2: EF Configurations

Dateien:

  1. src/Koogle.Infrastructure/Data/Configurations/BookingCategoryConfiguration.cs (neu)
  2. src/Koogle.Infrastructure/Data/Configurations/CashBookEntryConfiguration.cs (neu)
  3. src/Koogle.Infrastructure/Data/Configurations/ClubConfiguration.cs (erweitern)
  4. src/Koogle.Infrastructure/Data/AppDbContext.cs (erweitern)

BookingCategoryConfiguration:

builder.Property(x => x.Name).HasMaxLength(100).IsRequired();
builder.Property(x => x.Color).HasMaxLength(7); // #RRGGBB
builder.Property(x => x.Icon).HasMaxLength(50);
builder.HasQueryFilter(x => !x.IsDeleted);
builder.HasIndex(x => new { x.ClubId, x.Name }).HasFilter("[IsDeleted] = 0").IsUnique();

CashBookEntryConfiguration:

builder.Property(x => x.Amount).HasPrecision(10, 2).IsRequired();
builder.Property(x => x.Comment).HasMaxLength(500);
builder.Property(x => x.ReceiptReference).HasMaxLength(100);
builder.HasOne(x => x.Category).WithMany(c => c.CashBookEntries).HasForeignKey(x => x.CategoryId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(x => x.Day).WithMany().HasForeignKey(x => x.DayId).OnDelete(DeleteBehavior.SetNull);
builder.HasOne(x => x.Person).WithMany().HasForeignKey(x => x.PersonId).OnDelete(DeleteBehavior.SetNull);

Phase K3: Migration

dotnet ef migrations add AddKassenbuch --project src/Koogle.Infrastructure --startup-project src/Koogle.Web --context AppDbContext --output-dir Data/Migrations
dotnet ef database update -p src/Koogle.Infrastructure -s src/Koogle.Web --context AppDbContext

Phase K4: Kassenwart Role

Dateien:

  1. src/Koogle.Domain/Enums/UserRole.cs (erweitern)
  2. src/Koogle.Infrastructure/Security/IdentityRoleSeeder.cs (erweitern)
  3. src/Koogle.Infrastructure/Security/ClubRoleRequirement.cs (erweitern)
  4. src/Koogle.Infrastructure/DependencyInjection.cs (erweitern)

UserRole.cs:

public const string Treasurer = "Kassenwart";

ClubRoleRequirement.cs - Rank-Update:

static int Rank(string role) => role switch
{
    "Admin" => 4,
    "Kassenwart" => 3, // NEU
    "Editor" => 2,
    "Viewer" => 1,
    _ => 0
};

Policy Registration:

options.AddPolicy("ClubTreasurer", p =>
    p.Requirements.Add(new ClubRoleRequirement("Kassenwart")));

Phase K5: Repositories

Dateien:

  1. src/Koogle.Domain/Interfaces/IBookingCategoryRepository.cs (neu)
  2. src/Koogle.Domain/Interfaces/ICashBookEntryRepository.cs (neu)
  3. src/Koogle.Infrastructure/Repositories/BookingCategoryRepository.cs (neu)
  4. src/Koogle.Infrastructure/Repositories/CashBookEntryRepository.cs (neu)
  5. src/Koogle.Infrastructure/DependencyInjection.cs (erweitern)

IBookingCategoryRepository:

Task<List<BookingCategory>> GetByClubIdAsync(Guid clubId, bool includeInactive = false, CancellationToken ct = default);
Task<BookingCategory?> GetByIdAsync(Guid id, CancellationToken ct = default);
Task<BookingCategory?> GetSystemCategoryAsync(Guid clubId, string name, CancellationToken ct = default);
Task<BookingCategory> AddAsync(BookingCategory entity, CancellationToken ct = default);
Task<BookingCategory> UpdateAsync(BookingCategory entity, CancellationToken ct = default);

ICashBookEntryRepository:

Task<List<CashBookEntry>> GetByClubIdAsync(Guid clubId, DateTime? from, DateTime? to, CancellationToken ct = default);
Task<CashBookEntry?> GetByIdAsync(Guid id, CancellationToken ct = default);
Task<decimal> GetBalanceAsync(Guid clubId, DateTime? asOfDate = null, CancellationToken ct = default);
Task<CashBookEntry> AddAsync(CashBookEntry entity, CancellationToken ct = default);
Task<List<CashBookEntry>> GetByDayIdAsync(Guid dayId, CancellationToken ct = default);
Task<bool> ExistsMembershipFeeForMonthAsync(Guid clubId, int year, int month, CancellationToken ct = default);

Phase K6: DTOs

Dateien:

  1. src/Koogle.Application/DTOs/BookingCategoryDto.cs (neu)
  2. src/Koogle.Application/DTOs/CashBookEntryDto.cs (neu)
  3. src/Koogle.Application/DTOs/CashBookReportDto.cs (neu)

BookingCategoryDto.cs:

public record BookingCategoryDto(Guid Id, string Name, string? Description, BookingCategoryType CategoryType, bool IsSystemCategory, string? Color, string? Icon, bool IsActive);
public record CreateBookingCategoryDto(string Name, string? Description, BookingCategoryType CategoryType, string? Color, string? Icon);
public record UpdateBookingCategoryDto(Guid Id, string Name, string? Description, string? Color, string? Icon, bool IsActive);

CashBookEntryDto.cs:

public record CashBookEntryDto(Guid Id, Guid CategoryId, string CategoryName, CashBookEntryType EntryType, decimal Amount, DateTime BookingDate, string? Comment, string? ReceiptReference, Guid? DayId, Guid? PersonId, string? PersonName, DateTime CreatedAt);
public record CreateCashBookEntryDto(Guid CategoryId, CashBookEntryType EntryType, decimal Amount, DateTime BookingDate, string? Comment, string? ReceiptReference, Guid? PersonId);
public record UpdateCashBookEntryDto(Guid Id, Guid CategoryId, decimal Amount, DateTime BookingDate, string? Comment, string? ReceiptReference);

CashBookReportDto.cs:

public record CashBookReportDto(DateTime ReportStart, DateTime ReportEnd, decimal OpeningBalance, decimal ClosingBalance, decimal TotalIncome, decimal TotalExpense, List<CategorySummaryDto> IncomeByCategory, List<CategorySummaryDto> ExpenseByCategory, List<CashBookEntryDto> Entries);
public record CategorySummaryDto(Guid CategoryId, string CategoryName, decimal Total, int Count);
public record CreateMembershipFeesDto(int Year, int Month, decimal? OverrideAmount);

Phase K7: Service Interfaces

Dateien:

  1. src/Koogle.Application/Interfaces/IBookingCategoryService.cs (neu)
  2. src/Koogle.Application/Interfaces/ICashBookService.cs (neu)

IBookingCategoryService:

Task<List<BookingCategoryDto>> GetAllAsync(bool includeInactive = false, CancellationToken ct = default);
Task<BookingCategoryDto?> GetByIdAsync(Guid id, CancellationToken ct = default);
Task<BookingCategoryDto> CreateAsync(CreateBookingCategoryDto dto, CancellationToken ct = default);
Task<BookingCategoryDto> UpdateAsync(UpdateBookingCategoryDto dto, CancellationToken ct = default);
Task EnsureSystemCategoriesAsync(Guid clubId, CancellationToken ct = default);

ICashBookService:

Task<List<CashBookEntryDto>> GetEntriesAsync(DateTime? from = null, DateTime? to = null, CancellationToken ct = default);
Task<CashBookEntryDto> CreateAsync(CreateCashBookEntryDto dto, CancellationToken ct = default);
Task<CashBookEntryDto> UpdateAsync(UpdateCashBookEntryDto dto, CancellationToken ct = default);
Task<bool> DeleteAsync(Guid id, CancellationToken ct = default);
Task<decimal> GetCurrentBalanceAsync(CancellationToken ct = default);
Task<CashBookReportDto> GetMonthlyReportAsync(int year, int month, CancellationToken ct = default);
Task<CashBookReportDto> GetYearlyReportAsync(int year, CancellationToken ct = default);
Task CreatePenaltyEntriesForDayAsync(Guid dayId, CancellationToken ct = default);
Task<(int Created, bool HadExisting)> CreateMembershipFeesAsync(CreateMembershipFeesDto dto, CancellationToken ct = default);

Phase K8: Service Implementations

Dateien:

  1. src/Koogle.Application/Services/BookingCategoryService.cs (neu)
  2. src/Koogle.Application/Services/CashBookService.cs (neu)
  3. src/Koogle.Application/Mapping/CashBookMappingProfile.cs (neu)
  4. src/Koogle.Application/DependencyInjection.cs (erweitern)

CashBookService.CreatePenaltyEntriesForDayAsync Logic:

  1. Hole alle PersonExpenses für DayId mit ExpenseType = Monetary
  2. Gruppiere nach PersonId
  3. Für jede Person: CashBookEntry mit Category = "Spielstrafe"
  4. Setze PersonExpenses auf Done
  5. Setze DayId + PersonId für Nachverfolgbarkeit

Phase K9: Category Seeder

Dateien:

  1. src/Koogle.Application/Services/ClubService.cs (erweitern)

Bei Club-Erstellung aufrufen:

await _bookingCategoryService.EnsureSystemCategoriesAsync(club.Id, ct);

System-Kategorien:

  • "Spielstrafe" (Income, Green, Icon: Gavel)
  • "Mitgliedsbeitrag" (Income, Blue, Icon: CardMembership)
  • "Korrekturbuchung" (Income, Orange, Icon: Build)
  • "Saldoanpassung" (Income, Gray, Icon: Balance)

Phase K10: Day Close Integration

Dateien:

  1. src/Koogle.Application/Services/DayService.cs (erweitern)

In AdvanceStatusAsync nach Zeile 317:

if (day.Status == DayStatus.Closed)
{
    await CreateAbsentMemberExpensesAsync(context, day.Id, ct);

    // NEU: Kassenbuch-Einträge für Strafen
    try
    {
        await _cashBookService.CreatePenaltyEntriesForDayAsync(day.Id, ct);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to create cash book entries for day {DayId}", day.Id);
    }
}

Phase K11: Fluxor CategoryState

Dateien:

  1. src/Koogle.Web/Store/CategoryState/CategoryState.cs
  2. src/Koogle.Web/Store/CategoryState/CategoryActions.cs
  3. src/Koogle.Web/Store/CategoryState/CategoryReducers.cs
  4. src/Koogle.Web/Store/CategoryState/CategoryEffects.cs

State:

[FeatureState]
public record CategoryState
{
    public IReadOnlyList<BookingCategoryDto> Categories { get; init; } = [];
    public bool IsLoading { get; init; }
    public string? Error { get; init; }
}

Phase K12: Fluxor CashBookState

Dateien:

  1. src/Koogle.Web/Store/CashBookState/CashBookState.cs
  2. src/Koogle.Web/Store/CashBookState/CashBookActions.cs
  3. src/Koogle.Web/Store/CashBookState/CashBookReducers.cs
  4. src/Koogle.Web/Store/CashBookState/CashBookEffects.cs

State:

[FeatureState]
public record CashBookState
{
    public IReadOnlyList<CashBookEntryDto> Entries { get; init; } = [];
    public decimal CurrentBalance { get; init; }
    public CashBookReportDto? Report { get; init; }
    public DateTime FilterFrom { get; init; } = DateTime.Today.AddMonths(-1);
    public DateTime FilterTo { get; init; } = DateTime.Today;
    public bool IsLoading { get; init; }
    public string? Error { get; init; }
}

Phase K13: CashBook UI

Dateien:

  1. src/Koogle.Web/Components/Pages/CashBook/CashBook.razor
  2. src/Koogle.Web/Components/Pages/CashBook/CashBook.razor.cs
  3. src/Koogle.Web/Components/Pages/CashBook/CashBookEntryDialog.razor

Route: /cashbook Policy: ClubTreasurer

Features:

  • Kontostand-Anzeige oben
  • MudDataGrid mit Buchungen
  • Datumsfilter (Von/Bis)
  • Add/Edit/Delete Buttons
  • Farbcodierung: Einnahmen grün, Ausgaben rot
  • Kategorie-Filter Dropdown

Phase K14: Categories UI

Dateien:

  1. src/Koogle.Web/Components/Pages/CashBook/Categories.razor
  2. src/Koogle.Web/Components/Pages/CashBook/CategoryDialog.razor

Route: /cashbook/categories Policy: ClubTreasurer

Features:

  • MudDataGrid mit Kategorien
  • System-Kategorien markiert (kein Löschen)
  • Inaktive Kategorien ausgegraut
  • Farbvorschau
  • Icon-Auswahl

Phase K15: Reports UI

Dateien:

  1. src/Koogle.Web/Components/Pages/CashBook/Reports.razor
  2. src/Koogle.Web/Components/Pages/CashBook/Reports.razor.cs

Route: /cashbook/reports Policy: ClubViewer (Leserecht)

Features:

  • Monat/Jahr-Auswahl
  • Anfangs-/Endsaldo-Anzeige
  • Einnahmen/Ausgaben-Summen
  • Kategorie-Aufschlüsselung (MudChart)
  • Expandierbare Detailansicht
  • Export-Buttons (Excel, PDF)

Phase K16: Excel Export

Dateien:

  1. src/Koogle.Application/Interfaces/ICashBookExportService.cs (neu)
  2. src/Koogle.Application/Services/CashBookExportService.cs (neu)
  3. src/Koogle.Web/Koogle.Web.csproj (erweitern)

NuGet: ClosedXML

Excel-Struktur:

  • Sheet 1: Zusammenfassung (Salden, Summen)
  • Sheet 2: Kategorie-Aufschlüsselung
  • Sheet 3: Alle Buchungen

Phase K17: PDF Export

Dateien:

  1. src/Koogle.Application/Services/CashBookExportService.cs (erweitern)

NuGet: QuestPDF

PDF-Struktur:

  • Header mit Vereinsname, Berichtszeitraum
  • Saldo-Tabelle
  • Kategorie-Aufschlüsselung
  • Buchungsliste

Phase K18: Export Controller

Dateien:

  1. src/Koogle.Web/Controllers/CashBookController.cs (neu)
[Route("api/cashbook")]
[Authorize(Policy = "ClubTreasurer")]
public class CashBookController : ControllerBase
{
    [HttpGet("export/excel")]
    public async Task<IActionResult> ExportExcel(int year, int? month);

    [HttpGet("export/pdf")]
    public async Task<IActionResult> ExportPdf(int year, int? month);
}

Phase K19: Membership Fees Feature

Dateien:

  1. src/Koogle.Web/Components/Pages/CashBook/CashBook.razor (erweitern)
  2. src/Koogle.Web/Components/Pages/CashBook/MembershipFeeDialog.razor (neu)

Features:

  • Button "Mitgliedsbeiträge erfassen"
  • Dialog mit Monat/Jahr-Auswahl
  • Optionaler Betrags-Override
  • Warnung bei existierenden Beiträgen für Monat
  • Bestätigung trotz Warnung möglich
  • Erstellt Buchungen für alle aktiven Members

Phase K20: Club Settings Extension

Dateien:

  1. src/Koogle.Web/Components/Pages/Settings.razor (erweitern)
  2. src/Koogle.Application/DTOs/ClubDto.cs (erweitern)
  3. src/Koogle.Application/Services/ClubService.cs (erweitern)

Neue Settings-Tab "Kassenbuch":

  • Eröffnungssaldo (Decimal)
  • Monatlicher Mitgliedsbeitrag (Decimal)
  • Link zur Kategorienverwaltung

Phase K21: Navigation

Dateien:

  1. src/Koogle.Web/Components/Layout/NavMenu.razor (erweitern)

Menu-Struktur:

Kassenbuch (Icon: AccountBalance)
  ├─ Übersicht (/cashbook)
  ├─ Kategorien (/cashbook/categories)
  └─ Berichte (/cashbook/reports)

Phase K22: Unit Tests

Dateien:

  1. test/Koogle.Tests/Unit/Services/BookingCategoryServiceTests.cs (neu)
  2. test/Koogle.Tests/Unit/Services/CashBookServiceTests.cs (neu)

Test-Cases:

  • ✓ Create/Update/Delete Kategorie
  • ✓ System-Kategorie Löschschutz
  • ✓ Buchung erstellen/ändern/löschen
  • ✓ Kontostand-Berechnung
  • ✓ Report-Generierung
  • Day Close Integration
  • ✓ Mitgliedsbeitrags-Generierung
  • ✓ Duplikat-Warnung bei Beiträgen

Phase K23: Integration Tests

Dateien:

  1. test/Koogle.Tests/Integration/CashBookIntegrationTests.cs (neu)

Szenarien:

  • Day-Close-Workflow mit Kassenbuch-Einträgen
  • ✓ Mitgliedsbeitrags-Batch-Erstellung
  • ✓ Report-Generierung mit Datumsfiltern
  • Excel/PDF-Export

Berechtigungen Phase 5

Feature Policy
Buchungen CRUD ClubTreasurer
Kategorien CRUD ClubTreasurer
Berichte lesen ClubViewer
Berichte exportieren ClubTreasurer
Mitgliedsbeiträge erstellen ClubTreasurer
Kassenbuch-Settings ClubAdmin

Kritische Dateien

Datei Änderung
src/Koogle.Domain/Entities/Club.cs +InitialBalance, +MonthlyMembershipFee
src/Koogle.Application/Services/DayService.cs:317 Integration Day Close
src/Koogle.Infrastructure/Security/ClubRoleRequirement.cs Kassenwart Rang
src/Koogle.Infrastructure/DependencyInjection.cs Services + Policies
src/Koogle.Web/Components/Layout/NavMenu.razor Menu-Einträge

Zusammenfassung Phase 5

23 Phasen (K1-K23)~53 DateienKassenbuch-Modul komplett

Geschätzter Aufwand: ~25 Stunden

Kernfeatures:

  • Einnahmen/Ausgaben-Buchungen
  • Kategorien mit System-Kategorien
  • Automatische Strafen-Buchungen bei Day-Close
  • Mitgliedsbeitrags-Generierung
  • Monats-/Jahresberichte
  • Excel/PDF-Export
  • Neue Rolle "Kassenwart"