1478 lines
44 KiB
Markdown
1478 lines
44 KiB
Markdown
# 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<AppDbContext>, ClubId-Filter, Include Navigation Properties
|
||
|
||
---
|
||
|
||
### **Phase A3: DTOs erstellen**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Application/DTOs/ClubDto.cs` (ClubDto, CreateClubDto, UpdateClubDto)
|
||
2. `src/Koogle.Application/DTOs/PersonDto.cs` (PersonDto, CreatePersonDto, UpdatePersonDto)
|
||
3. `src/Koogle.Application/DTOs/ExpenseDto.cs` (ExpenseDto, CreateExpenseDto, UpdateExpenseDto)
|
||
4. `src/Koogle.Application/DTOs/DayDto.cs` erweitern (DayDto, CreateDayDto, UpdateDayDto, DayParticipantDto)
|
||
5. `src/Koogle.Application/DTOs/PersonExpenseDto.cs` (PersonExpenseDto, CreatePersonExpenseDto, DayEvaluationDto, PersonDayEvaluationDto)
|
||
|
||
---
|
||
|
||
### **Phase A4: Service Interfaces erstellen**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Application/Interfaces/IClubService.cs`
|
||
2. `src/Koogle.Application/Interfaces/IPersonService.cs`
|
||
3. `src/Koogle.Application/Interfaces/IExpenseService.cs`
|
||
4. `src/Koogle.Application/Interfaces/IDayService.cs` (erweitern)
|
||
5. `src/Koogle.Application/Interfaces/IPersonExpenseService.cs`
|
||
|
||
**Methoden:** GetAllAsync, GetByIdAsync, CreateAsync, UpdateAsync, DeleteAsync + spezifische Methoden
|
||
|
||
---
|
||
|
||
### **Phase A5: Service Implementations erstellen**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Application/Services/ClubService.cs`
|
||
2. `src/Koogle.Application/Services/PersonService.cs`
|
||
3. `src/Koogle.Application/Services/ExpenseService.cs`
|
||
4. `src/Koogle.Application/Services/DayService.cs` (erweitern)
|
||
5. `src/Koogle.Application/Services/PersonExpenseService.cs`
|
||
|
||
**Dependencies:** Repository, ICurrentClubContext, ICurrentUserService, IMapper
|
||
**Business Logic:** ClubId-Injection, Audit-Felder, Validierung (mittels FluentValidation)
|
||
|
||
---
|
||
|
||
### **Phase A6: AutoMapper Profiles erstellen**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Application/Mapping/ClubMappingProfile.cs`
|
||
2. `src/Koogle.Application/Mapping/PersonMappingProfile.cs`
|
||
3. `src/Koogle.Application/Mapping/ExpenseMappingProfile.cs`
|
||
4. `src/Koogle.Application/Mapping/DayMappingProfile.cs`
|
||
5. `src/Koogle.Application/Mapping/PersonExpenseMappingProfile.cs`
|
||
|
||
**Pattern:** CreateMap<Entity, Dto>() bidirektional
|
||
|
||
---
|
||
|
||
### **Phase A7: DI Registration**
|
||
|
||
**Dateien ändern:**
|
||
1. `src/Koogle.Infrastructure/DependencyInjection.cs` (5 Repositories registrieren)
|
||
2. `src/Koogle.Application/DependencyInjection.cs` (5 Services registrieren)
|
||
|
||
---
|
||
|
||
### **Phase B1: UserService erweitern**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Application/Interfaces/IUserService.cs` erweitern
|
||
2. `src/Koogle.Application/Services/UserService.cs` erweitern
|
||
3. `src/Koogle.Application/DTOs/UserDto.cs` erweitern (RegisterUserDto, ResetPasswordDto, UpdateUserProfileDto)
|
||
|
||
**Neue Methoden:**
|
||
- RegisterUserAsync
|
||
- RequestPasswordResetAsync
|
||
- ResetPasswordAsync
|
||
- UpdateProfileAsync
|
||
- AssignUserToClubAsync, RemoveUserFromClubAsync
|
||
- AssignClubRoleAsync, RemoveClubRoleAsync
|
||
|
||
---
|
||
|
||
### **Phase B2: Register Page**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Components/Pages/Account/Register.razor`
|
||
|
||
**Features:** Email, Password, DisplayName, Optional ClubName für initiale Zuordnung
|
||
|
||
---
|
||
|
||
### **Phase B3: Password Reset Pages**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Components/Pages/Account/ForgotPassword.razor`
|
||
2. `src/Koogle.Web/Components/Pages/Account/ResetPassword.razor`
|
||
|
||
**Flow:** Email eingeben → Token per Email → Passwort zurücksetzen
|
||
|
||
---
|
||
|
||
### **Phase B4: Profile Page**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Components/Pages/Account/Profile.razor`
|
||
|
||
**Features:** DisplayName, Locale, TimeZone, Club-Memberships anzeigen, Standard-Club setzen
|
||
|
||
---
|
||
|
||
### **Phase B5: Admin Users Page**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Components/Pages/Admin/Users.razor`
|
||
|
||
**Features:** User-Liste, Club-Zuordnungen, Rollen pro Club
|
||
|
||
---
|
||
|
||
### **Phase C1: ClubState (Fluxor)**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Store/ClubState/ClubState.cs`
|
||
2. `src/Koogle.Web/Store/ClubState/ClubActions.cs`
|
||
3. `src/Koogle.Web/Store/ClubState/ClubReducers.cs`
|
||
4. `src/Koogle.Web/Store/ClubState/ClubEffects.cs`
|
||
|
||
**Actions:** Load, Create, Update, Delete
|
||
|
||
---
|
||
|
||
### **Phase C2: Club Pages**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Components/Pages/Admin/Clubs.razor`
|
||
|
||
**Features:** MudTable, CRUD-Dialogs, SuperAdmin only
|
||
|
||
---
|
||
|
||
### **Phase D1: PersonState (Fluxor)**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Store/PersonState/PersonState.cs`
|
||
2. `src/Koogle.Web/Store/PersonState/PersonActions.cs`
|
||
3. `src/Koogle.Web/Store/PersonState/PersonReducers.cs`
|
||
4. `src/Koogle.Web/Store/PersonState/PersonEffects.cs`
|
||
|
||
---
|
||
|
||
### **Phase D2: Person Pages**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Components/Pages/Persons/Persons.razor`
|
||
|
||
**Features:** MudTable mit Filter (Member/Guest), CRUD
|
||
|
||
---
|
||
|
||
### **Phase D3: ExpenseState (Fluxor)**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Store/ExpenseState/ExpenseState.cs`
|
||
2. `src/Koogle.Web/Store/ExpenseState/ExpenseActions.cs`
|
||
3. `src/Koogle.Web/Store/ExpenseState/ExpenseReducers.cs`
|
||
4. `src/Koogle.Web/Store/ExpenseState/ExpenseEffects.cs`
|
||
|
||
---
|
||
|
||
### **Phase D4: Expense Pages**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Components/Pages/Expenses/Expenses.razor`
|
||
|
||
**Features:** MudTable mit Expense-Vorlagen, CRUD
|
||
|
||
---
|
||
|
||
### **Phase E1: DayState (Fluxor)**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Store/DayState/DayState.cs`
|
||
2. `src/Koogle.Web/Store/DayState/DayActions.cs`
|
||
3. `src/Koogle.Web/Store/DayState/DayReducers.cs`
|
||
4. `src/Koogle.Web/Store/DayState/DayEffects.cs`
|
||
|
||
**State:** Days, SelectedDay, SelectedDayExpenses, AvailablePersons
|
||
|
||
---
|
||
|
||
### **Phase E2: Days List Page**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Components/Pages/Days/Days.razor`
|
||
|
||
**Features:** MudTable mit Jahr-Filter, Create Day
|
||
|
||
---
|
||
|
||
### **Phase E3: Day Details Page**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Components/Pages/Days/DayDetails.razor`
|
||
|
||
**Features:**
|
||
- Day-Header (Datum, Status)
|
||
- Status-Workflow Buttons (New→Started→Closed)
|
||
- Teilnehmer-Sektion (Add/Remove)
|
||
- PersonExpense-Sektion
|
||
|
||
---
|
||
|
||
### **Phase E4: PersonExpense Management**
|
||
|
||
**Components in DayDetails:**
|
||
- PersonExpense-Tabelle
|
||
- Add Expense Dialog (Person auswählen, Expense auswählen, Preis editierbar wenn IsVariable)
|
||
- Delete PersonExpense (nur in New/Started)
|
||
- IsInverse Logic: 1 Person auswählen → alle anderen bekommen Expense
|
||
|
||
---
|
||
|
||
### **Phase F1: Dashboard Page**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Components/Pages/Dashboard.razor`
|
||
|
||
**Features:** Summary Cards, Recent Days, Top Penalty Recipients
|
||
|
||
---
|
||
|
||
### **Phase F2: Evaluation Components**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Components/Shared/DayEvaluationComponent.razor`
|
||
2. `src/Koogle.Web/Components/Shared/PersonEvaluationComponent.razor`
|
||
|
||
---
|
||
|
||
### **Phase F3: Navigation finalisieren**
|
||
|
||
**Dateien ändern:**
|
||
1. `src/Koogle.Web/Components/Layout/NavMenu.razor`
|
||
|
||
**Features:** Dashboard, Spieltage, Stammdaten, Admin, Profil
|
||
|
||
---
|
||
|
||
### **Phase G1: MembershipStatus + UserProfileClub erweitern**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Domain/Enums/MembershipStatus.cs` (neu)
|
||
- Pending, Approved, Rejected
|
||
|
||
2. `src/Koogle.Domain/Entities/UserProfileClub.cs` (erweitern)
|
||
- MembershipStatus Status (default: Pending)
|
||
- string? RejectionReason
|
||
- DateTime? ApprovedAt, Guid? ApprovedById
|
||
- DateTime? RejectedAt, Guid? RejectedById
|
||
|
||
3. Migration erstellen
|
||
|
||
---
|
||
|
||
### **Phase G2: ClubInvitation Entity**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Domain/Entities/ClubInvitation.cs` (neu)
|
||
- Guid Id, Guid ClubId
|
||
- string Token (unique, für URL)
|
||
- DateTime ExpiresAt, DateTime CreatedAt, Guid CreatedById
|
||
- int? MaxUses (null = unbegrenzt), int UsedCount
|
||
|
||
2. `src/Koogle.Infrastructure/Data/AppDbContext.cs` - DbSet hinzufügen
|
||
3. Migration erstellen
|
||
|
||
---
|
||
|
||
### **Phase G3: IEmailService (Stub)**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Application/Interfaces/IEmailService.cs` (neu)
|
||
- SendMembershipRequestNotificationAsync
|
||
- SendMembershipApprovedAsync
|
||
- SendMembershipRejectedAsync
|
||
|
||
2. `src/Koogle.Infrastructure/Services/StubEmailService.cs` (neu)
|
||
- Logging statt echtem Versand
|
||
- TODO-Kommentare für SMTP
|
||
|
||
3. DI Registration
|
||
|
||
---
|
||
|
||
### **Phase G4: Services erweitern (Membership-Logik)**
|
||
|
||
**UserService erweitern:**
|
||
- RequestClubMembershipAsync(userProfileId, clubId)
|
||
- RequestClubMembershipByNameAsync(userProfileId, clubName)
|
||
- RequestClubMembershipByInviteAsync(userProfileId, inviteToken)
|
||
- ApproveMembershipAsync(userProfileId, clubId, approvedById)
|
||
- RejectMembershipAsync(userProfileId, clubId, rejectedById, reason)
|
||
- GetPendingMembershipsAsync(clubId)
|
||
|
||
**ClubService erweitern:**
|
||
- CreateInvitationAsync(clubId, createdById, expiresAt, maxUses)
|
||
- GetInvitationByTokenAsync(token)
|
||
- ValidateInvitationAsync(token)
|
||
|
||
---
|
||
|
||
### **Phase G5: Dashboard Pending-Widget**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Components/Shared/PendingMembershipsWidget.razor` (neu)
|
||
- Anzahl ausstehender Anträge für Club-Admins
|
||
- Link zur Admin Users Page
|
||
|
||
2. Dashboard.razor erweitern - Widget für Admins einbinden
|
||
|
||
---
|
||
|
||
### **Phase G6: Admin Users Page erweitern**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Components/Pages/Admin/Users.razor` (ändern)
|
||
- Tab/Filter für "Ausstehende Anträge"
|
||
- Approve/Reject Buttons
|
||
- Reject-Dialog mit Begründungsfeld
|
||
|
||
---
|
||
|
||
### **Phase G7: Club-Beitritt UI**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Components/Pages/Account/JoinClub.razor` (neu)
|
||
- Eingabefeld für Club-Name
|
||
- Suche/Validierung
|
||
- Beitrittsantrag senden
|
||
- Erfolgsmeldung "Antrag gestellt"
|
||
|
||
2. Dashboard.razor erweitern
|
||
- "Keinem Club zugeordnet" Meldung mit Link zu JoinClub
|
||
|
||
---
|
||
|
||
### **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<AppDbContext> 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<GamePerson> GamePersons { get; set; }
|
||
public Guid ClubId { get; set; }
|
||
public Club Club { get; set; }
|
||
|
||
// NEU:
|
||
public string GameType { get; set; } = ""; // "Training", "Shit"
|
||
public GameStatus Status { get; set; } // Active, Completed, Aborted
|
||
public DateTime? StartedAt { get; set; }
|
||
public DateTime? CompletedAt { get; set; }
|
||
public byte[] RowVersion { get; set; } // Optimistic Concurrency
|
||
}
|
||
|
||
public enum GameStatus { Active, Completed, Aborted }
|
||
```
|
||
|
||
---
|
||
|
||
## GameData JSON Struktur (Beispiel Training)
|
||
|
||
```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
|
||
{
|
||
/// <summary>
|
||
/// Gets all expenses linked to a trigger type for the current club.
|
||
/// </summary>
|
||
Task<IEnumerable<ExpenseDto>> GetExpensesForTriggerAsync(ExpenseTriggerType type);
|
||
|
||
/// <summary>
|
||
/// Fires a trigger, creating PersonExpenses for linked expenses.
|
||
/// </summary>
|
||
/// <param name="type">The trigger type (e.g., ExpensePoint)</param>
|
||
/// <param name="personId">Person receiving the expense</param>
|
||
/// <param name="dayId">Day context</param>
|
||
/// <param name="gameId">Optional game context</param>
|
||
/// <param name="multiplier">Multiplier for expense price (e.g., remaining points)</param>
|
||
Task FireTriggerAsync(
|
||
ExpenseTriggerType type,
|
||
Guid personId,
|
||
Guid dayId,
|
||
Guid? gameId = null,
|
||
int multiplier = 1);
|
||
}
|
||
```
|
||
|
||
**Logik FireTriggerAsync:**
|
||
1. Hole alle Expenses die mit Trigger-Typ verknüpft sind (via ExpenseTrigger Join-Table)
|
||
2. Für jede Expense: Erstelle PersonExpense mit `Price = Expense.Price × multiplier`
|
||
3. multiplier = Restpunkte, z.B. im Scheiss-Spiel
|
||
|
||
---
|
||
|
||
### **Phase H1: GameState Foundation**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Store/GameState/GameState.cs`
|
||
2. `src/Koogle.Web/Store/GameState/GameActions.cs`
|
||
3. `src/Koogle.Web/Store/GameState/GameReducers.cs`
|
||
4. `src/Koogle.Web/Store/GameState/GameEffects.cs`
|
||
5. `src/Koogle.Domain/Enums/GameType.cs`
|
||
6. `src/Koogle.Domain/Enums/PinStatus.cs`
|
||
|
||
**State-Struktur:**
|
||
```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<GameSnapshot> UndoStack { get; init; } // Unbegrenzt
|
||
public IReadOnlyList<GameSummaryDto> CompletedGames { get; init; }
|
||
public bool IsLoading { get; init; }
|
||
public string? Error { get; init; }
|
||
public bool IsConcurrencyConflict { get; init; } // RowVersion conflict
|
||
}
|
||
|
||
public record ThrowPanelState(
|
||
bool IsStarted,
|
||
PinStatus Pin1, PinStatus Pin2, PinStatus Pin3,
|
||
PinStatus Pin4, PinStatus Pin5, PinStatus Pin6,
|
||
PinStatus Pin7, PinStatus Pin8, PinStatus Pin9,
|
||
int ThrowsPerRound,
|
||
int ThrowCounterPerRound,
|
||
ThrowMode ThrowMode,
|
||
int TotalThrowCounter,
|
||
bool BellValue
|
||
);
|
||
|
||
public record ParticipantsState(
|
||
Guid[] PlayerIds,
|
||
int CurrentPlayerIndex,
|
||
ParticipantsMode Mode
|
||
);
|
||
|
||
public record GameSnapshot(
|
||
ThrowPanelState ThrowPanel,
|
||
ParticipantsState Participants,
|
||
object GameModel
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### **Phase H2: Pin Input Components**
|
||
|
||
**Dateien (NEU SCHREIBEN):**
|
||
1. `src/Koogle.Web/Components/Game/Pin.razor`
|
||
2. `src/Koogle.Web/Components/Game/PinPanel.razor`
|
||
3. `src/Koogle.Web/Components/Game/NumberPanel.razor`
|
||
4. `src/Koogle.Web/Components/Game/ThrowPanel.razor`
|
||
5. `src/Koogle.Web/Components/Game/GameInputPanel.razor`
|
||
|
||
**Pin-Layout (9 Kegel):**
|
||
```
|
||
①
|
||
② ③
|
||
④ ⑤ ⑥
|
||
⑦ ⑧
|
||
⑨
|
||
```
|
||
|
||
**Features:**
|
||
- Pin.razor: Einzelner Kegel, klickbar, Status (Standing/Fallen/Disabled)
|
||
- PinPanel.razor: 9-Pin-Layout, Touch-optimiert
|
||
- NumberPanel.razor: Schnelleingabe 1-9, Wurf-Button, Glocke
|
||
- ThrowPanel.razor: Wurf-Bestätigung, Rinnen links/rechts
|
||
- GameInputPanel.razor: Orchestriert alles, zeigt aktuellen Spieler
|
||
|
||
---
|
||
|
||
### **Phase H3: Game Definition Framework**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Domain/Interfaces/IGameDefinition.cs`
|
||
2. `src/Koogle.Application/Games/GameProgress.cs`
|
||
3. `src/Koogle.Application/Games/IGameLogicService.cs`
|
||
4. `src/Koogle.Application/Games/GameDefinitionRegistry.cs`
|
||
5. `src/Koogle.Application/DependencyInjection.cs` (erweitern)
|
||
|
||
**IGameDefinition:**
|
||
```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<TrainingGameModel>(json),
|
||
"Shit" => JsonSerializer.Deserialize<ShitGameModel>(json),
|
||
_ => throw new NotSupportedException($"Unknown game type: {gameType}")
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### **Phase H4: Training Game**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Application/Games/Training/TrainingGameDefinition.cs`
|
||
2. `src/Koogle.Application/Games/Training/TrainingGameModel.cs`
|
||
3. `src/Koogle.Application/Games/Training/TrainingGameLogicService.cs`
|
||
4. `src/Koogle.Web/Components/Game/Training/TrainingSetup.razor`
|
||
5. `src/Koogle.Web/Components/Game/Training/TrainingBoard.razor`
|
||
|
||
**TrainingGameModel:**
|
||
```csharp
|
||
public record TrainingGameModel
|
||
{
|
||
public Dictionary<Guid, PlayerStats> PlayerStatistics { get; init; }
|
||
}
|
||
|
||
public record PlayerStats
|
||
{
|
||
public int ThrowCount { get; set; }
|
||
public int PinCount { get; set; }
|
||
public int CircleCount { get; set; } // Kränze (8+1)
|
||
public int StrikeCount { get; set; } // Alle 9
|
||
}
|
||
```
|
||
|
||
**TrainingSetup.razor:**
|
||
- ThrowMode: Reposition (in die Vollen) / Decrease (Abräumen)
|
||
- ThrowsPerRound: 1-5
|
||
- ParticipantsMode: GameLogic (Rotation) / FreeToChoose
|
||
|
||
**TrainingBoard.razor (Tafel):**
|
||
| Spieler | Würfe | Kegel | Kränze | ⌀ |
|
||
|---------|-------|-------|--------|---|
|
||
| Max | 24 | 156 | 3 | 6.5 |
|
||
|
||
---
|
||
|
||
### **Phase H5: Scheiss-Spiel + Trigger-Integration**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Application/Games/Shit/ShitGameDefinition.cs`
|
||
2. `src/Koogle.Application/Games/Shit/ShitGameModel.cs`
|
||
3. `src/Koogle.Application/Games/Shit/ShitGameLogicService.cs`
|
||
4. `src/Koogle.Web/Components/Game/Shit/ShitSetup.razor`
|
||
5. `src/Koogle.Web/Components/Game/Shit/ShitBoard.razor`
|
||
6. Integration mit ITriggerService
|
||
|
||
**ShitGameModel:**
|
||
```csharp
|
||
public record ShitGameModel
|
||
{
|
||
public int ShitNumber { get; init; } // 1-9 (Scheiss-Zahl)
|
||
public int StartNumber { get; init; } // 10-1000 (Startzahl)
|
||
public Dictionary<Guid, int> PlayerPoints { get; init; }
|
||
public int CollectedPoints { get; set; }
|
||
public Guid? WinnerId { get; set; }
|
||
}
|
||
```
|
||
|
||
**Spiellogik:**
|
||
1. Spieler wirft
|
||
2. Wenn Scheiss-Zahl oder Rinne → bis dahin gesammelte Punkte werden zum Konto addiert, nächster Spieler
|
||
3. Sonst → Punkte werden gesammelt und subtrahiert, denn der Spieler entscheidet die Punkte mitzunehmenan den nächsten Spieler zu übergeben (subtrahieren). Spiel-Spezifischer "Weiter-Button" wird benötigt.
|
||
4. Erster mit 0 Punkten → **GEWINNER**
|
||
5. Spiel endet, für ein Spieler 0 erreicht hat. Alle danderen sind Verlierer: `ITriggerService.FireTriggerAsync(ExpensePoint, personId, dayId, gameId, restpunkte)`
|
||
|
||
**ShitSetup.razor:**
|
||
- Scheiss-Zahl (1-9, Default: 5)
|
||
- Start-Zahl (10-1000, Default: 50)
|
||
- Warnung wenn kein ExpensePoint-Trigger konfiguriert
|
||
|
||
**ShitBoard.razor (Tafel):**
|
||
| Spieler | Punkte | Status |
|
||
|---------|--------|--------|
|
||
| Max | 23 | 🎯 |
|
||
| Anna | 0 | 🏆 GEWINNER |
|
||
|
||
---
|
||
|
||
### **Phase H6: Game Setup Dialog**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Components/Game/GameSetupDialog.razor`
|
||
2. `src/Koogle.Web/Components/Game/GameTypeSelector.razor`
|
||
3. `src/Koogle.Web/Components/Game/ParticipantSelector.razor`
|
||
4. `src/Koogle.Web/Components/Game/CommonSetupOptions.razor`
|
||
|
||
**Flow:**
|
||
1. Dialog öffnen
|
||
2. Spieltyp auswählen (Training, Scheiss-Spiel)
|
||
3. DynamicComponent rendert spieltyp-spezifisches Setup
|
||
4. Teilnehmer aus Day-Participants auswählen
|
||
5. "Spiel starten" → `StartGameAction` dispatch
|
||
|
||
---
|
||
|
||
### **Phase H7: DayDetails Integration**
|
||
|
||
**Dateien ändern:**
|
||
1. `src/Koogle.Web/Components/Pages/Days/DayDetails.razor`
|
||
2. `src/Koogle.Web/Components/Game/GameBoardPanel.razor`
|
||
3. `src/Koogle.Web/Components/Game/CompletedGamesList.razor`
|
||
|
||
**DayDetails Erweiterung:**
|
||
```razor
|
||
@code {
|
||
private int _activeTabIndex = 0;
|
||
}
|
||
|
||
<MudTabs @bind-ActivePanelIndex="_activeTabIndex">
|
||
<MudTabPanel Text="Details" Icon="@Icons.Material.Filled.Info">
|
||
<!-- Bestehend: Participants + Expenses + Quick-Assign -->
|
||
</MudTabPanel>
|
||
|
||
<MudTabPanel Text="Eingabe" Icon="@Icons.Material.Filled.SportsScore"
|
||
Disabled="@(!GameState.Value.IsGameActive)">
|
||
<GameInputPanel />
|
||
</MudTabPanel>
|
||
|
||
<MudTabPanel Text="Tafel" Icon="@Icons.Material.Filled.TableChart"
|
||
Disabled="@(!GameState.Value.IsGameActive)">
|
||
<GameBoardPanel />
|
||
</MudTabPanel>
|
||
</MudTabs>
|
||
|
||
<!-- Spiel starten Button (nur wenn Day.Status == Started und kein aktives Spiel) -->
|
||
@if (SelectedDay?.Status == DayStatus.Started && !GameState.Value.IsGameActive)
|
||
{
|
||
<MudButton OnClick="OpenGameSetupDialog" Color="Color.Primary">
|
||
Neues Spiel starten
|
||
</MudButton>
|
||
}
|
||
|
||
<!-- Liste abgeschlossener Spiele -->
|
||
<CompletedGamesList DayId="DayId" />
|
||
```
|
||
|
||
---
|
||
|
||
### **Phase H8: Undo (Unbegrenzt)**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Web/Store/GameState/GameState.cs` (UndoStack bereits definiert)
|
||
2. `src/Koogle.Web/Components/Game/UndoButton.razor`
|
||
|
||
**Logic:**
|
||
- Jeder Wurf erstellt GameSnapshot → auf UndoStack pushen
|
||
- Stack ist **unbegrenzt** (komplette Spiel-Historie)
|
||
- "Letzten Wurf rückgängig" Button
|
||
- Bei Undo: Pop vom Stack, State wiederherstellen
|
||
|
||
---
|
||
|
||
### **Phase H9: DB Persistence & Recovery**
|
||
|
||
**Dateien:**
|
||
1. `src/Koogle.Application/Interfaces/IGamePersistenceService.cs`
|
||
2. `src/Koogle.Application/Services/GamePersistenceService.cs`
|
||
3. `src/Koogle.Web/Store/GameState/GameEffects.cs` (erweitern)
|
||
4. `src/Koogle.Application/DTOs/GameStateDto.cs` (JSON-Serialisierung)
|
||
|
||
**Methoden:**
|
||
- `SaveGameStateAsync(Guid gameId, GameState state)` - Nach jedem Wurf
|
||
- `LoadActiveGameAsync(Guid dayId)` - Bei Page-Load
|
||
- `GetCompletedGamesAsync(Guid dayId)` - Für Spiele-Liste
|
||
|
||
**Persistenz-Flow:**
|
||
```
|
||
1. Spiel starten → Game Entity mit Status=Active erstellen
|
||
2. Nach jedem Wurf:
|
||
- GameState → JSON serialisieren (inkl. UndoStack)
|
||
- Game.GameData updaten (Debounced, max 1x/500ms)
|
||
- RowVersion prüfen → bei Konflikt: IsConcurrencyConflict = true
|
||
3. Page Reload / Andere Session:
|
||
- LoadActiveGameAsync(dayId)
|
||
- Wenn Active Game existiert → GameState wiederherstellen
|
||
4. Spiel beenden:
|
||
- Status = Completed, CompletedAt = now
|
||
- Finales GameData speichern
|
||
```
|
||
|
||
**Concurrency-Handling:**
|
||
```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<GameHubService>();
|
||
|
||
// Endpoints
|
||
app.MapHub<GameHub>("/gamehub");
|
||
```
|
||
|
||
**SignalR Hub:**
|
||
```csharp
|
||
public class GameHub : Hub<IGameHubClient>
|
||
{
|
||
public async Task JoinGame(Guid gameId)
|
||
=> await Groups.AddToGroupAsync(Context.ConnectionId, gameId.ToString());
|
||
|
||
public async Task LeaveGame(Guid gameId)
|
||
=> await Groups.RemoveFromGroupAsync(Context.ConnectionId, gameId.ToString());
|
||
}
|
||
|
||
public interface IGameHubClient
|
||
{
|
||
Task GameStateUpdated(GameStateDto state);
|
||
Task GameEnded(GameResultDto result);
|
||
Task ConcurrencyConflict(Guid gameId);
|
||
}
|
||
```
|
||
|
||
**Auto-Reconnect (Client-Side):**
|
||
```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
|