# 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 Integration des Game-Management-Systems aus KoogleApp in Koogle.Web mit Clean Architecture. 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 | JSON in `Game.GameData` nach Spielende speichern | | Scheiss-Spiel Ende | Erster mit 0 Punkten **gewinnt**, andere bekommen Strafen nach Restpunkten | | Strafen-Berechnung | Fixbetrag pro Restpunkt (z.B. 0.10€ × 30 Punkte = 3.00€) | ## 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 | Port aus KoogleApp mit neuem Namespace | | Persistenz | In-Memory während Spiel → DB (Game.GameData) bei Ende | ## Existierende Entity ```csharp // Koogle.Domain/Entities/Game.cs - bereits vorhanden public class Game : BaseEntity { public string GameData { get; set; } = "{}"; // JSON für Spielergebnis 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; } } // → GameType Property hinzufügen (string oder Enum) ``` ## Architektur ``` Domain: GameType Enum, IGameDefinition Interface Application: Games/ mit GameProgress, IGameLogicService, Training/, Shit/ Web: Store/GameState/, Components/Game/ ``` ## Umsetzungsreihenfolge Phase 2 | ☐ | Phase | Bereich | Beschreibung | Dateien | |---|-------|---------|--------------|---------| | ☐ | H1 | Foundation | GameState Fluxor | 5 | | ☐ | H2 | Components | Pin Input Components | 5 | | ☐ | H3 | Framework | Game Definition Framework | 5 | | ☐ | H4 | Games | Training Game | 5 | | ☐ | H5 | Games | Scheiss-Spiel | 5 | | ☐ | H6 | UI | Game Setup Dialog | 4 | | ☐ | H7 | Integration | DayDetails Integration | 3 | | ☐ | H8 | Features | Undo/Redo | 3 | | ☐ | H9 | Features | Session Persistence | 2 | | ☐ | H10 | Testing | Game Tests | 3 | **Geschätzte Dateien: ~42 neue/modifizierte** --- ## Detaillierte Phasen ### **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` **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 UndoRedoState UndoRedo { get; init; } public IReadOnlyList CompletedGames { get; init; } public bool IsLoading { get; init; } public string? Error { get; init; } } 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 ); ``` --- ### **Phase H2: Pin Input Components** **Dateien (Port aus KoogleApp):** 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 (Port):** - BeforeThrowState, AfterThrowState - PinCount(), IsCircle() Extension Methods - NextRound(), EndOfGame() Helpers --- ### **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** **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` **ShitGameModel:** ```csharp public record ShitGameModel { public int ShitNumber { get; init; } // 1-9 (Scheiss-Zahl) public int StartNumber { get; init; } // 10-1000 (Startzahl) public decimal PricePerPoint { get; init; } // 0.01-1.00 (€ pro Restpunkt) 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 → gesammelte Punkte werden vom Konto abgezogen, nächster Spieler 3. Sonst → Punkte werden gesammelt (addiert) 4. Erster mit 0 Punkten → **GEWINNER** 5. Spiel endet, alle anderen bekommen Strafe: `Restpunkte × PricePerPoint` **ShitSetup.razor:** - Scheiss-Zahl (1-9, Default: 5) - Start-Zahl (10-1000, Default: 50) - **Betrag pro Punkt** (0.01€ - 1.00€, Default: 0.10€) **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/Redo** **Dateien:** 1. `src/Koogle.Web/Store/GameState/UndoRedoState.cs` 2. `src/Koogle.Web/Store/GameState/UndoRedoActions.cs` 3. `src/Koogle.Web/Components/Game/UndoRedoButtons.razor` **Logic:** - Jeder Wurf erstellt Snapshot - Stack für Undo (max 20 Einträge) - "Letzten Wurf rückgängig" Button --- ### **Phase H9: Session Persistence** **Dateien:** 1. `src/Koogle.Web/wwwroot/js/gameSession.js` 2. `src/Koogle.Web/Store/GameState/GameEffects.cs` (erweitern) **Mechanismus:** - Bei State-Änderung: SessionStorage speichern - Bei Page-Load: SessionStorage prüfen - Wenn Session für aktuellen DayId existiert: Restore anbieten --- ### **Phase H10: Testing** **Dateien:** 1. `test/Koogle.Tests/Application/Games/TrainingGameLogicServiceTests.cs` 2. `test/Koogle.Tests/Application/Games/ShitGameLogicServiceTests.cs` 3. `test/Koogle.Tests/Web/GameStateReducersTests.cs` **Test-Cases:** - Pin-Count Berechnung - Kranz-Erkennung (8 äußere + Mitte steht) - Scheiss-Spiel: Gewinner bei 0 - Scheiss-Spiel: Strafen-Berechnung - Reducer: State-Transformationen --- ## Scheiss-Spiel Spielende-Logik ``` 1. Spieler erreicht 0 Punkte → GEWINNER (WinnerId setzen) 2. Spiel endet automatisch (IsGameActive = false) 3. Für jeden Verlierer: - Strafe = Restpunkte × Betrag_pro_Punkt - PersonExpense erstellen (ExpenseType: GamePenalty) 4. Game.GameData speichern (JSON mit Ergebnis) 5. CompletedGames-Liste aktualisieren ``` --- ## Kritische Dateien (KoogleApp → Koogle.Web Port) | Quelle (KoogleApp) | Ziel (Koogle.Web) | |--------------------|-------------------| | `src/KoogleApp/Games/GameProgress.cs` | `src/Koogle.Application/Games/GameProgress.cs` | | `src/KoogleApp/Store/Game/ThrowPanel/State.cs` | In `GameState.cs` integriert | | `src/KoogleApp/Games/Training/GameTrainingService.cs` | `src/Koogle.Application/Games/Training/TrainingGameLogicService.cs` | | `src/KoogleApp/Components/Controls/PinPanel.razor` | `src/Koogle.Web/Components/Game/PinPanel.razor` | | `src/KoogleApp/Components/Controls/NumberPanel.razor` | `src/Koogle.Web/Components/Game/NumberPanel.razor` | --- ## Migration ```bash # GameType Property zu Game Entity hinzufügen dotnet ef migrations add AddGameType --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 ``` --- ## Berechtigungen Phase 2 - **Spiel starten/beenden**: ClubEditor+ - **Würfe eingeben**: ClubEditor+ - **Tafel ansehen**: ClubViewer+ --- ## Zusammenfassung Phase 2 **10 Phasen (H1-H10)** → **~42 Dateien** → **Detaillierte Spielverwaltung** Bereit für Implementierung Phase H1