From 4dae965c98c3b8713a047aa8ee8a879ab762c58c Mon Sep 17 00:00:00 2001 From: beo3000 Date: Sat, 3 Jan 2026 10:59:18 +0100 Subject: [PATCH] added cashbook planning --- docs/IMPLEMENTATION_PLAN.md | 639 ++++++++++++++++++++++++++++++++++++ 1 file changed, 639 insertions(+) diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index ad81c2b..3f84517 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1591,3 +1591,642 @@ DeathBox ist ein Eliminationsspiel nach dem Hangman-Prinzip. Spieler sammeln Str ## 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"