|
|
|
|
@ -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"
|
|
|
|
|
|