# 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() 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 --- ### **Phase G8: Einladungslink-Handling** **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 Dateien** → **MVP 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) ```csharp // 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 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) ```json { "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:** ```csharp public interface ITriggerService { /// /// Gets all expenses linked to a trigger type for the current club. /// Task> GetExpensesForTriggerAsync(ExpenseTriggerType type); /// /// Fires a trigger, creating PersonExpenses for linked expenses. /// /// The trigger type (e.g., ExpensePoint) /// Person receiving the expense /// Day context /// Optional game context /// Multiplier for expense price (e.g., remaining points) 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:** ```csharp [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 UndoStack { get; init; } // Unbegrenzt public IReadOnlyList 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:** ```csharp 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):** ```csharp public static class GameModelFactory { public static object Deserialize(string json, string gameType) { return gameType switch { "Training" => JsonSerializer.Deserialize(json), "Shit" => JsonSerializer.Deserialize(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:** ```csharp public record TrainingGameModel { public Dictionary 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:** ```csharp public record ShitGameModel { public int ShitNumber { get; init; } // 1-9 (Scheiss-Zahl) public int StartNumber { get; init; } // 10-1000 (Startzahl) public Dictionary 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:** ```razor @code { private int _activeTabIndex = 0; } @if (SelectedDay?.Status == DayStatus.Started && !GameState.Value.IsGameActive) { Neues Spiel starten } ``` --- ### **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:** ```csharp 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:** ```csharp // Services builder.Services.AddSignalR(); builder.Services.AddScoped(); // Endpoints app.MapHub("/gamehub"); ``` **SignalR Hub:** ```csharp public class GameHub : Hub { 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):** ```csharp _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 ```bash # 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:** ```csharp 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 Dateien** → **Detaillierte 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 Dateien** → **Trigger-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 Dateien** → **DeathBox 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 ```csharp // 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 CashBookEntries { get; set; } = []; } ``` ### CashBookEntry Entity ```csharp // 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 ```csharp // Neue Properties in Club.cs public decimal InitialBalance { get; set; } // Eröffnungssaldo public decimal MonthlyMembershipFee { get; set; } // Monatsbeitrag ``` ### Neue Enums ```csharp // 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:** ```csharp 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:** ```csharp 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** ```bash 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:** ```csharp public const string Treasurer = "Kassenwart"; ``` **ClubRoleRequirement.cs - Rank-Update:** ```csharp static int Rank(string role) => role switch { "Admin" => 4, "Kassenwart" => 3, // NEU "Editor" => 2, "Viewer" => 1, _ => 0 }; ``` **Policy Registration:** ```csharp 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:** ```csharp Task> GetByClubIdAsync(Guid clubId, bool includeInactive = false, CancellationToken ct = default); Task GetByIdAsync(Guid id, CancellationToken ct = default); Task GetSystemCategoryAsync(Guid clubId, string name, CancellationToken ct = default); Task AddAsync(BookingCategory entity, CancellationToken ct = default); Task UpdateAsync(BookingCategory entity, CancellationToken ct = default); ``` **ICashBookEntryRepository:** ```csharp Task> GetByClubIdAsync(Guid clubId, DateTime? from, DateTime? to, CancellationToken ct = default); Task GetByIdAsync(Guid id, CancellationToken ct = default); Task GetBalanceAsync(Guid clubId, DateTime? asOfDate = null, CancellationToken ct = default); Task AddAsync(CashBookEntry entity, CancellationToken ct = default); Task> GetByDayIdAsync(Guid dayId, CancellationToken ct = default); Task 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:** ```csharp 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:** ```csharp 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:** ```csharp public record CashBookReportDto(DateTime ReportStart, DateTime ReportEnd, decimal OpeningBalance, decimal ClosingBalance, decimal TotalIncome, decimal TotalExpense, List IncomeByCategory, List ExpenseByCategory, List 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:** ```csharp Task> GetAllAsync(bool includeInactive = false, CancellationToken ct = default); Task GetByIdAsync(Guid id, CancellationToken ct = default); Task CreateAsync(CreateBookingCategoryDto dto, CancellationToken ct = default); Task UpdateAsync(UpdateBookingCategoryDto dto, CancellationToken ct = default); Task EnsureSystemCategoriesAsync(Guid clubId, CancellationToken ct = default); ``` **ICashBookService:** ```csharp Task> GetEntriesAsync(DateTime? from = null, DateTime? to = null, CancellationToken ct = default); Task CreateAsync(CreateCashBookEntryDto dto, CancellationToken ct = default); Task UpdateAsync(UpdateCashBookEntryDto dto, CancellationToken ct = default); Task DeleteAsync(Guid id, CancellationToken ct = default); Task GetCurrentBalanceAsync(CancellationToken ct = default); Task GetMonthlyReportAsync(int year, int month, CancellationToken ct = default); Task 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:** ```csharp 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:** ```csharp 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:** ```csharp [FeatureState] public record CategoryState { public IReadOnlyList 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:** ```csharp [FeatureState] public record CashBookState { public IReadOnlyList 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) ```csharp [Route("api/cashbook")] [Authorize(Policy = "ClubTreasurer")] public class CashBookController : ControllerBase { [HttpGet("export/excel")] public async Task ExportExcel(int year, int? month); [HttpGet("export/pdf")] public async Task 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/Services/BookingCategoryServiceTests.cs` (neu) 2. `test/Koogle.Tests/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:** - Kompletter 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 Dateien** → **Kassenbuch-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"