added cashbook planning

This commit is contained in:
beo3000 2026-01-03 10:59:18 +01:00
parent 4cee96408b
commit 4dae965c98
1 changed files with 639 additions and 0 deletions

View File

@ -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<CashBookEntry> 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<List<BookingCategory>> GetByClubIdAsync(Guid clubId, bool includeInactive = false, CancellationToken ct = default);
Task<BookingCategory?> GetByIdAsync(Guid id, CancellationToken ct = default);
Task<BookingCategory?> GetSystemCategoryAsync(Guid clubId, string name, CancellationToken ct = default);
Task<BookingCategory> AddAsync(BookingCategory entity, CancellationToken ct = default);
Task<BookingCategory> UpdateAsync(BookingCategory entity, CancellationToken ct = default);
```
**ICashBookEntryRepository:**
```csharp
Task<List<CashBookEntry>> GetByClubIdAsync(Guid clubId, DateTime? from, DateTime? to, CancellationToken ct = default);
Task<CashBookEntry?> GetByIdAsync(Guid id, CancellationToken ct = default);
Task<decimal> GetBalanceAsync(Guid clubId, DateTime? asOfDate = null, CancellationToken ct = default);
Task<CashBookEntry> AddAsync(CashBookEntry entity, CancellationToken ct = default);
Task<List<CashBookEntry>> GetByDayIdAsync(Guid dayId, CancellationToken ct = default);
Task<bool> ExistsMembershipFeeForMonthAsync(Guid clubId, int year, int month, CancellationToken ct = default);
```
---
### **Phase K6: DTOs**
**Dateien:**
1. `src/Koogle.Application/DTOs/BookingCategoryDto.cs` (neu)
2. `src/Koogle.Application/DTOs/CashBookEntryDto.cs` (neu)
3. `src/Koogle.Application/DTOs/CashBookReportDto.cs` (neu)
**BookingCategoryDto.cs:**
```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<CategorySummaryDto> IncomeByCategory, List<CategorySummaryDto> ExpenseByCategory, List<CashBookEntryDto> Entries);
public record CategorySummaryDto(Guid CategoryId, string CategoryName, decimal Total, int Count);
public record CreateMembershipFeesDto(int Year, int Month, decimal? OverrideAmount);
```
---
### **Phase K7: Service Interfaces**
**Dateien:**
1. `src/Koogle.Application/Interfaces/IBookingCategoryService.cs` (neu)
2. `src/Koogle.Application/Interfaces/ICashBookService.cs` (neu)
**IBookingCategoryService:**
```csharp
Task<List<BookingCategoryDto>> GetAllAsync(bool includeInactive = false, CancellationToken ct = default);
Task<BookingCategoryDto?> GetByIdAsync(Guid id, CancellationToken ct = default);
Task<BookingCategoryDto> CreateAsync(CreateBookingCategoryDto dto, CancellationToken ct = default);
Task<BookingCategoryDto> UpdateAsync(UpdateBookingCategoryDto dto, CancellationToken ct = default);
Task EnsureSystemCategoriesAsync(Guid clubId, CancellationToken ct = default);
```
**ICashBookService:**
```csharp
Task<List<CashBookEntryDto>> GetEntriesAsync(DateTime? from = null, DateTime? to = null, CancellationToken ct = default);
Task<CashBookEntryDto> CreateAsync(CreateCashBookEntryDto dto, CancellationToken ct = default);
Task<CashBookEntryDto> UpdateAsync(UpdateCashBookEntryDto dto, CancellationToken ct = default);
Task<bool> DeleteAsync(Guid id, CancellationToken ct = default);
Task<decimal> GetCurrentBalanceAsync(CancellationToken ct = default);
Task<CashBookReportDto> GetMonthlyReportAsync(int year, int month, CancellationToken ct = default);
Task<CashBookReportDto> GetYearlyReportAsync(int year, CancellationToken ct = default);
Task CreatePenaltyEntriesForDayAsync(Guid dayId, CancellationToken ct = default);
Task<(int Created, bool HadExisting)> CreateMembershipFeesAsync(CreateMembershipFeesDto dto, CancellationToken ct = default);
```
---
### **Phase K8: Service Implementations**
**Dateien:**
1. `src/Koogle.Application/Services/BookingCategoryService.cs` (neu)
2. `src/Koogle.Application/Services/CashBookService.cs` (neu)
3. `src/Koogle.Application/Mapping/CashBookMappingProfile.cs` (neu)
4. `src/Koogle.Application/DependencyInjection.cs` (erweitern)
**CashBookService.CreatePenaltyEntriesForDayAsync Logic:**
1. Hole alle PersonExpenses für DayId mit ExpenseType = Monetary
2. Gruppiere nach PersonId
3. Für jede Person: CashBookEntry mit Category = "Spielstrafe"
4. Setze PersonExpenses auf Done
5. Setze DayId + PersonId für Nachverfolgbarkeit
---
### **Phase K9: Category Seeder**
**Dateien:**
1. `src/Koogle.Application/Services/ClubService.cs` (erweitern)
**Bei Club-Erstellung aufrufen:**
```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<BookingCategoryDto> Categories { get; init; } = [];
public bool IsLoading { get; init; }
public string? Error { get; init; }
}
```
---
### **Phase K12: Fluxor CashBookState**
**Dateien:**
1. `src/Koogle.Web/Store/CashBookState/CashBookState.cs`
2. `src/Koogle.Web/Store/CashBookState/CashBookActions.cs`
3. `src/Koogle.Web/Store/CashBookState/CashBookReducers.cs`
4. `src/Koogle.Web/Store/CashBookState/CashBookEffects.cs`
**State:**
```csharp
[FeatureState]
public record CashBookState
{
public IReadOnlyList<CashBookEntryDto> Entries { get; init; } = [];
public decimal CurrentBalance { get; init; }
public CashBookReportDto? Report { get; init; }
public DateTime FilterFrom { get; init; } = DateTime.Today.AddMonths(-1);
public DateTime FilterTo { get; init; } = DateTime.Today;
public bool IsLoading { get; init; }
public string? Error { get; init; }
}
```
---
### **Phase K13: CashBook UI**
**Dateien:**
1. `src/Koogle.Web/Components/Pages/CashBook/CashBook.razor`
2. `src/Koogle.Web/Components/Pages/CashBook/CashBook.razor.cs`
3. `src/Koogle.Web/Components/Pages/CashBook/CashBookEntryDialog.razor`
**Route:** `/cashbook`
**Policy:** `ClubTreasurer`
**Features:**
- Kontostand-Anzeige oben
- MudDataGrid mit Buchungen
- Datumsfilter (Von/Bis)
- Add/Edit/Delete Buttons
- Farbcodierung: Einnahmen grün, Ausgaben rot
- Kategorie-Filter Dropdown
---
### **Phase K14: Categories UI**
**Dateien:**
1. `src/Koogle.Web/Components/Pages/CashBook/Categories.razor`
2. `src/Koogle.Web/Components/Pages/CashBook/CategoryDialog.razor`
**Route:** `/cashbook/categories`
**Policy:** `ClubTreasurer`
**Features:**
- MudDataGrid mit Kategorien
- System-Kategorien markiert (kein Löschen)
- Inaktive Kategorien ausgegraut
- Farbvorschau
- Icon-Auswahl
---
### **Phase K15: Reports UI**
**Dateien:**
1. `src/Koogle.Web/Components/Pages/CashBook/Reports.razor`
2. `src/Koogle.Web/Components/Pages/CashBook/Reports.razor.cs`
**Route:** `/cashbook/reports`
**Policy:** `ClubViewer` (Leserecht)
**Features:**
- Monat/Jahr-Auswahl
- Anfangs-/Endsaldo-Anzeige
- Einnahmen/Ausgaben-Summen
- Kategorie-Aufschlüsselung (MudChart)
- Expandierbare Detailansicht
- Export-Buttons (Excel, PDF)
---
### **Phase K16: Excel Export**
**Dateien:**
1. `src/Koogle.Application/Interfaces/ICashBookExportService.cs` (neu)
2. `src/Koogle.Application/Services/CashBookExportService.cs` (neu)
3. `src/Koogle.Web/Koogle.Web.csproj` (erweitern)
**NuGet:** `ClosedXML`
**Excel-Struktur:**
- Sheet 1: Zusammenfassung (Salden, Summen)
- Sheet 2: Kategorie-Aufschlüsselung
- Sheet 3: Alle Buchungen
---
### **Phase K17: PDF Export**
**Dateien:**
1. `src/Koogle.Application/Services/CashBookExportService.cs` (erweitern)
**NuGet:** `QuestPDF`
**PDF-Struktur:**
- Header mit Vereinsname, Berichtszeitraum
- Saldo-Tabelle
- Kategorie-Aufschlüsselung
- Buchungsliste
---
### **Phase K18: Export Controller**
**Dateien:**
1. `src/Koogle.Web/Controllers/CashBookController.cs` (neu)
```csharp
[Route("api/cashbook")]
[Authorize(Policy = "ClubTreasurer")]
public class CashBookController : ControllerBase
{
[HttpGet("export/excel")]
public async Task<IActionResult> ExportExcel(int year, int? month);
[HttpGet("export/pdf")]
public async Task<IActionResult> ExportPdf(int year, int? month);
}
```
---
### **Phase K19: Membership Fees Feature**
**Dateien:**
1. `src/Koogle.Web/Components/Pages/CashBook/CashBook.razor` (erweitern)
2. `src/Koogle.Web/Components/Pages/CashBook/MembershipFeeDialog.razor` (neu)
**Features:**
- Button "Mitgliedsbeiträge erfassen"
- Dialog mit Monat/Jahr-Auswahl
- Optionaler Betrags-Override
- Warnung bei existierenden Beiträgen für Monat
- Bestätigung trotz Warnung möglich
- Erstellt Buchungen für alle aktiven Members
---
### **Phase K20: Club Settings Extension**
**Dateien:**
1. `src/Koogle.Web/Components/Pages/Settings.razor` (erweitern)
2. `src/Koogle.Application/DTOs/ClubDto.cs` (erweitern)
3. `src/Koogle.Application/Services/ClubService.cs` (erweitern)
**Neue Settings-Tab "Kassenbuch":**
- Eröffnungssaldo (Decimal)
- Monatlicher Mitgliedsbeitrag (Decimal)
- Link zur Kategorienverwaltung
---
### **Phase K21: Navigation**
**Dateien:**
1. `src/Koogle.Web/Components/Layout/NavMenu.razor` (erweitern)
**Menu-Struktur:**
```
Kassenbuch (Icon: AccountBalance)
├─ Übersicht (/cashbook)
├─ Kategorien (/cashbook/categories)
└─ Berichte (/cashbook/reports)
```
---
### **Phase K22: Unit Tests**
**Dateien:**
1. `test/Koogle.Tests/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"