Compare commits

...

3 Commits

Author SHA1 Message Date
beo3000 7a5580e57d mod default culture de-DE 2026-01-02 21:09:37 +01:00
beo3000 51b8c2dbd4 edit users in club-settings 2026-01-02 20:58:49 +01:00
beo3000 f9e72d095b mod home 2026-01-02 20:22:01 +01:00
7 changed files with 374 additions and 277 deletions

View File

@ -1,15 +1,15 @@
1. Allgemeine Rahmenbedingungen
Die Finanzverwaltung wird als Modul / Erweiterung einer bestehenden Software umgesetzt.
Die Finanzverwaltung wird als Modul / Erweiterung der bestehenden Software umgesetzt.
Eine Benutzerverwaltung existiert bereits und wird genutzt.
Der Kegelverein ist kein eingetragener Verein, es bestehen keine steuerlichen oder rechtlichen Sonderanforderungen.
Es gibt ein Vereinskonto und eine Währung (EUR).
Es gibt ein Vereinskonto und eine Währung (EUR) pro angelegtem Verin.
2. Benutzer- und Rollenmodell
2.1 Rollen
Es wird eine zusätzliche Rolle für Finanzdaten integriert (z. B. „Kassenwart“, „Finanzverwaltung“).
Es wird eine zusätzliche Rolle für Finanzdaten integriert (z. B. „Kassenwart“ oder „Finanzverwaltung“).
Nur Benutzer mit dieser Rolle dürfen:
@ -23,275 +23,132 @@ Finanzdaten ändern oder löschen
Berichte exportieren
2.2 Zugriffsrechte
Benutzer ohne Finanzrolle haben keinen Zugriff auf Finanzdaten.
## 2.2 Zugriffsrechte
Benutzer ohne Finanzrolle haben Änderungsrechte auf Finanzdaten.
Leserechte und Schreibrechte müssen klar getrennt umsetzbar sein (falls von der bestehenden Software unterstützt).
Leserechte und Schreibrechte müssen klar getrennt umsetzbar sein.
3. Kategorienverwaltung
3.1 Kategorien für Einnahmen und Ausgaben
# 3. Kategorienverwaltung
## 3.1 Kategorien für Einnahmen und Ausgaben
Einnahmen und Ausgaben müssen kategorisiert werden.
Kategorien sind:
- frei definierbar
- beliebig erweiterbar
- editierbar (Name ändern)
- deaktivierbar, ohne bestehende Buchungen zu verändern
frei definierbar
beliebig erweiterbar
editierbar (Name ändern)
deaktivierbar, ohne bestehende Buchungen zu verändern
3.2 Kategorietypen
## 3.2 Kategorietypen
Es muss mindestens eine Unterscheidung geben zwischen:
- Einnahmen-Kategorien
- Ausgaben-Kategorien
- Kategorien sollten für bessere Usability zusätzlich mit einer Farbe und einer Gruppierung oder Icons versehbar sein
Einnahmen-Kategorien
Ausgaben-Kategorien
4. Buchungen (Kassenbuch)
4.1 Erfassung von Einnahmen und Ausgaben
# 4. Buchungen (Kassenbuch)
## 4.1 Erfassung von Einnahmen und Ausgaben
Für jede Buchung müssen folgende Felder vorhanden sein:
Buchungsart: Einnahme / Ausgabe
- Buchungsart: Einnahme / Ausgabe
- Betrag (positiv, in EUR)
- Kategorie
- Buchungsdatum (frei wählbar, nicht zwingend aktuelles Datum)
- Optionaler Kommentar / Beschreibung
- Optionaler externer Belegverweis (z. B. Belegnummer oder Text)
Betrag (positiv, in EUR)
Kategorie
Buchungsdatum (frei wählbar, nicht zwingend aktuelles Datum)
Optionaler Kommentar / Beschreibung
Optionaler externer Belegverweis (z. B. Belegnummer oder Text)
4.2 Belege
## 4.2 Belege
Kein Upload von Dokumenten
Stattdessen:
- Freitextfeld oder Referenz zur Verknüpfung mit extern abgelegten Belegen
Freitextfeld oder Referenz zur Verknüpfung mit extern abgelegten Belegen
5. Kontostand und Kontenabgleich
5.1 Kontostand
# 5. Kontostand und Kontenabgleich
## 5.1 Kontostand
Die Software führt einen internen Kontostand auf Basis der erfassten Buchungen.
Es existiert genau ein Konto.
Es existiert genau ein Konto pro Verein.
5.2 Manueller Abgleich
## 5.2 Manueller Abgleich
Der Abgleich mit dem realen Bankkonto erfolgt manuell.
Abweichungen können durch Korrekturbuchungen ausgeglichen werden.
Korrekturbuchungen:
- sind normale Buchungen
- müssen eindeutig als solche erkennbar sein (z. B. spezielle Kategorie oder Kennzeichnung)
- Dafür werden doe Kategorien "Korrekturbuchung" und "Saldoanpassung" mit ausgeliefert, die nicht löschbar ist
sind normale Buchungen
müssen eindeutig als solche erkennbar sein (z. B. spezielle Kategorie oder Kennzeichnung)
6. Auswertungen und Berichte
6.1 Standardberichte
# 6. Auswertungen und Berichte
## 6.1 Standardberichte
Folgende Berichte müssen verfügbar sein:
- Monatsübersicht
- Jahresübersicht
Monatsübersicht
## 6.2 Inhalt der Berichte
- Anfangs- und Endsaldo
- Summe der Einnahmen
- Summe der Ausgaben
- Aufschlüsselung nach Kategorien
- Optionale Detailansicht aller Buchungen im Zeitraum
Jahresübersicht
6.2 Inhalt der Berichte
Anfangs- und Endsaldo
Summe der Einnahmen
Summe der Ausgaben
Aufschlüsselung nach Kategorien
Optionale Detailansicht aller Buchungen im Zeitraum
6.3 Export
## 6.3 Export
Berichte müssen exportierbar sein als:
- Excel (z. B. XLSX)
- PDF
Excel (z. B. XLSX)
PDF
7. Änderungsprotokoll (Audit-Log)
7.1 Protokollierung
# 7. Änderungsprotokoll (Audit-Log)
## 7.1 Protokollierung
Alle Änderungen an Finanzdaten müssen protokolliert werden:
- Neuanlage von Buchungen
- Änderungen an Buchungen
- Löschungen von Buchungen
Hierfür kann die BaseEntity genutzt werden. Es kein zusätzlicher Aufwand erforderlich.
Neuanlage von Buchungen
Änderungen an Buchungen
Löschungen von Buchungen
Änderungen an Kategorien
7.2 Protokollinhalt
Das Protokoll enthält mindestens:
Datum und Uhrzeit der Änderung
Benutzer
Art der Änderung (Anlage / Änderung / Löschung)
Alte und neue Werte (soweit sinnvoll darstellbar)
7.3 Einsicht
Das Änderungsprotokoll ist für berechtigte Benutzer einsehbar.
8. Bedienbarkeit und Usability
# 8. Bedienbarkeit und Usability
Zielgruppe sind softwareerfahrene Benutzer.
Es wird keine Benutzeranleitung benötigt.
Es wird kleine Benutzeranleitung benötigt.
Fokus liegt auf:
klaren Masken
übersichtlichen Tabellen
schneller Datenerfassung
- klaren Masken
- übersichtlichen Tabellen
- schneller Datenerfassung
Konsistenz mit der bestehenden Software-Oberfläche ist wünschenswert.
9. Technische und funktionale Abgrenzungen (Nicht-Ziele)
# 9. Technische und funktionale Abgrenzungen (Nicht-Ziele)
Folgende Funktionen sind nicht Bestandteil der ersten Version:
Automatische Zahlungserinnerungen
- Automatische Zahlungserinnerungen
- Import von Bankumsätzen
- Schnittstellen zu anderen Systemen
- Dokumenten-Uploads
- Mehrere Konten oder Währungen
- Steuerliche oder rechtliche Auswertungen
Import von Bankumsätzen
Schnittstellen zu anderen Systemen
Dokumenten-Uploads
Mehrere Konten oder Währungen
Steuerliche oder rechtliche Auswertungen
10. Erweiterbarkeit (optional, aber empfehlenswert)
# 10. Erweiterbarkeit (optional, aber empfehlenswert)
Die Architektur sollte so gestaltet sein, dass spätere Erweiterungen möglich sind, z. B.:
Automatischer Kontoimport
- Automatischer Kontoimport
- Mehrere Konten
- Erinnerungen
- Schnittstellen
Mehrere Konten
# 11. Integration in bestehen Anwendung
Erinnerungen
## 11.1
Abschluss eines Spieltages erzeugt neue Buchung
- beim Abschluss eines Spieltages wird die Strafen-Summe pro Spieler und Tag dem Kassenbuch hinzugefügt
- Dafür wird eine Kategorie "Spielstrafe" mit ausgeliefert, die nicht löschbar ist
- Die Strafen die die Buchung auslösen werden damit als erledigt gekennzeichnet
Schnittstellen
## 11.2
Mitgliedsbeiträge
- Die Vereineinstellungen werden um einen monatlichen Mitgliedsbeitrag erweitert
- Dafür wird eine Kategorie "Mitgliedsbeitrag" mit ausgeliefert, die nicht löschbar ist
- Die Mitgliedsbeiträge können über einen Button für einen Montag ermittelt und ins Kassenbuch aufgenommen werden
Wenn du möchtest, kann ich dir im nächsten Schritt z. B.
ein Datenmodell (Tabellen / Entitäten)
User Stories
oder eine Minimal-Feature-Liste für ein MVP
ableiten sag einfach Bescheid.
Du:
ja, mach das
ChatGPT:
Perfekt 👍
Dann liefere ich dir alle drei Punkte in sinnvoller Reihenfolge und aufeinander aufbauend:
Datenmodell (Entitäten & Felder)
User Stories
MVP-Feature-Liste (klar abgegrenzt)
So kannst du direkt mit Design oder Implementierung starten.
1. Datenmodell (fachlich / logisch)
1.1 Benutzer
Existiert bereits wird hier nur referenziert
User
user_id (PK)
name
… (bestehende Felder)
rolle_finanz (Boolean oder Rollenreferenz)
1.2 Kategorien
Kategorie
kategorie_id (PK)
name
typ (EINNAHME | AUSGABE)
aktiv (Boolean)
erstellt_am
erstellt_von (user_id)
1.3 Buchungen (Kassenbuch)
Buchung
buchung_id (PK)
buchungsart (EINNAHME | AUSGABE)
betrag (Decimal, > 0)
buchungsdatum (Date)
kategorie_id (FK)
kommentar (Text, optional)
belegreferenz (Text, optional)
ist_korrekturbuchung (Boolean)
erstellt_am (Timestamp)
erstellt_von (user_id)
geändert_am (Timestamp, optional)
geändert_von (user_id, optional)
gelöscht (Boolean, Soft Delete empfohlen)
1.4 Änderungsprotokoll (Audit-Log)
Finanz_Audit_Log
log_id (PK)
entität (BUCHUNG | KATEGORIE)
entität_id
aktion (CREATE | UPDATE | DELETE)
feldname (optional, bei UPDATE)
alter_wert (Text)
neuer_wert (Text)
zeitpunkt (Timestamp)
benutzer_id (user_id)
Empfehlung: bei UPDATE pro Feld ein Logeintrag
1.5 Abgeleitete Werte (kein persistentes Objekt)
Kontostand
wird dynamisch berechnet:
Summe(Einnahmen) Summe(Ausgaben)
2. User Stories (fachlich, umsetzungsnah)
Kategorien
Als Finanznutzer möchte ich Kategorien für Einnahmen und Ausgaben anlegen,
@ -330,51 +187,3 @@ um sie weiterzugeben oder zu archivieren.
Sicherheit & Nachvollziehbarkeit
Als Verein möchten wir, dass alle Änderungen an Finanzdaten protokolliert werden,
damit Transparenz und Nachvollziehbarkeit gewährleistet sind.
3. MVP Minimal Viable Product
3.1 Muss-Funktionen (Version 1.0)
✅ Pflicht
Finanzrolle integrieren
Kategorien anlegen / bearbeiten / deaktivieren
Einnahmen & Ausgaben erfassen
Buchungen bearbeiten & (soft) löschen
Korrekturbuchungen
Kontostandberechnung
Monats- & Jahresübersichten
Export als Excel & PDF
Vollständiges Änderungsprotokoll
3.2 Sollte-Funktionen (Version 1.1)
🔶 Optional, aber sinnvoll
Filter (Zeitraum, Kategorie)
Summen pro Kategorie
Kennzeichnung auffälliger Buchungen (z. B. hohe Beträge)
Schnellanlage über Standardkategorien
3.3 Explizit ausgeschlossen (vorerst)
❌ Nicht Teil des MVP
Bankimport
Mehrere Konten
Mehrere Währungen
Dokumentenupload
Automatische Erinnerungen
Externe Schnittstellen

View File

@ -613,6 +613,9 @@ public class UserService : IUserService
await _appDb.SaveChangesAsync(ct);
// Assign default Viewer role to new member
await AssignClubRoleAsync(userProfileId, clubId, "Viewer", approvedById, ct);
// Send notification to user
var identityUser = await _userManager.FindByIdAsync(membership.UserProfile.IdentityUserId.ToString());
if (identityUser?.Email != null)

View File

@ -25,7 +25,14 @@
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
? new InteractiveServerRenderMode(prerender: false)
: new InteractiveServerRenderMode(prerender: true);
private IComponentRenderMode? RenderModeForPage
{
get
{
var path = HttpContext.Request.Path;
var disablePrerender = path.StartsWithSegments("/Account")
|| path.StartsWithSegments("/settings");
return new InteractiveServerRenderMode(prerender: !disablePrerender);
}
}
}

View File

@ -0,0 +1,132 @@
@using Koogle.Application.DTOs
@using Koogle.Application.Interfaces
@using Koogle.Domain.Enums
@inject IUserService UserService
@inject ISnackbar Snackbar
<MudDialog>
<DialogContent>
<MudText Typo="Typo.subtitle1" Class="mb-3">Rollen fuer @User.DisplayName</MudText>
<MudPaper Class="pa-3" Elevation="1">
<MudStack Row="true" Spacing="2" Wrap="Wrap.Wrap">
@foreach (var role in _availableRoles)
{
var hasRole = _currentRoles.Contains(role);
<MudChip T="string"
Color="@(hasRole ? Color.Primary : Color.Default)"
Variant="@(hasRole ? Variant.Filled : Variant.Outlined)"
OnClick="@(() => ToggleRole(role, hasRole))"
Disabled="_isProcessing">
@GetRoleDisplayName(role)
</MudChip>
}
</MudStack>
</MudPaper>
<MudDivider Class="my-4" />
<MudButton Size="Size.Small"
Color="Color.Error"
Variant="Variant.Text"
StartIcon="@Icons.Material.Filled.RemoveCircle"
OnClick="RemoveFromClub"
Disabled="_isProcessing">
Aus Verein entfernen
</MudButton>
</DialogContent>
<DialogActions>
<MudButton OnClick="Close">Schliessen</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter] public UserDto User { get; set; } = null!;
[Parameter] public Guid ClubId { get; set; }
[Parameter] public Guid CurrentUserProfileId { get; set; }
// Available roles for club-admin (no SuperAdmin)
private readonly string[] _availableRoles = { UserRole.Viewer, UserRole.Editor, UserRole.Admin };
private List<string> _currentRoles = new();
private bool _isProcessing;
private bool _hasChanges;
protected override void OnInitialized()
{
var membership = User.ClubMemberships.FirstOrDefault(m => m.ClubId == ClubId);
_currentRoles = membership?.Roles.ToList() ?? new List<string>();
}
private static string GetRoleDisplayName(string role) => role switch
{
UserRole.Viewer => "Betrachter",
UserRole.Editor => "Bearbeiter",
UserRole.Admin => "Administrator",
_ => role
};
private async Task ToggleRole(string role, bool currentlyHasRole)
{
_isProcessing = true;
try
{
bool success;
if (currentlyHasRole)
{
success = await UserService.RemoveClubRoleAsync(User.ProfileId, ClubId, role);
}
else
{
success = await UserService.AssignClubRoleAsync(User.ProfileId, ClubId, role, CurrentUserProfileId);
}
if (success)
{
if (currentlyHasRole)
_currentRoles.Remove(role);
else
_currentRoles.Add(role);
_hasChanges = true;
Snackbar.Add(currentlyHasRole ? "Rolle entfernt" : "Rolle hinzugefuegt", Severity.Success);
StateHasChanged();
}
else
{
Snackbar.Add("Fehler beim Aendern der Rolle", Severity.Error);
}
}
finally
{
_isProcessing = false;
}
}
private async Task RemoveFromClub()
{
_isProcessing = true;
try
{
var success = await UserService.RemoveUserFromClubAsync(User.ProfileId, ClubId);
if (success)
{
_hasChanges = true;
Snackbar.Add("Aus Verein entfernt", Severity.Success);
MudDialog.Close(DialogResult.Ok(_hasChanges));
}
else
{
Snackbar.Add("Fehler beim Entfernen", Severity.Error);
}
}
finally
{
_isProcessing = false;
}
}
private void Close() => MudDialog.Close(DialogResult.Ok(_hasChanges));
}

View File

@ -144,6 +144,15 @@
</MudStack>
</MudCard>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudCard Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
<MudStack AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.AccountBalance" Color="Color.Warning" Size="Size.Large" />
<MudText Typo="Typo.subtitle1" Class="font-weight-bold" Align="Align.Center">Kassenbuch</MudText>
<MudText Typo="Typo.body2" Align="Align.Center" Color="Color.Secondary">autom. Buchungen, Kontoabgleich u.v.m wird euer Kassenwart lieben</MudText>
</MudStack>
</MudCard>
</MudItem>
</MudGrid>
</div>
@ -155,7 +164,7 @@
<MudStack Spacing="2">
<MudStack Row AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Check" Color="Color.Success" />
<MudText>Rollenbasierte Berechtigungen (Viewer/Editor/Admin)</MudText>
<MudText>Rollenbasierte Berechtigungen (Viewer/Editor/Schatzmeister/Admin)</MudText>
</MudStack>
<MudStack Row AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Check" Color="Color.Success" />

View File

@ -196,6 +196,61 @@ else
</MudCardContent>
</MudCard>
</MudTabPanel>
<MudTabPanel Text="Mitglieder" BadgeData="@(_clubMembers.Count)" BadgeColor="Color.Primary">
<MudCard Class="mt-4">
<MudCardContent>
@if (_isLoadingMembers)
{
<MudProgressCircular Indeterminate="true" Size="Size.Small" />
}
else if (!_clubMembers.Any())
{
<MudAlert Severity="Severity.Info">Keine Mitglieder vorhanden.</MudAlert>
}
else
{
<MudTable Items="_clubMembers" Dense="true" Hover="true">
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>E-Mail</MudTh>
<MudTh>Rollen</MudTh>
<MudTh>Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.DisplayName</MudTd>
<MudTd DataLabel="E-Mail">@context.Identity.Email</MudTd>
<MudTd DataLabel="Rollen">
@{
var membership = context.ClubMemberships.FirstOrDefault(m => m.ClubId == CurrentClubContext.ClubId);
if (membership != null && membership.Roles.Any())
{
@foreach (var role in membership.Roles)
{
<MudChip T="string" Size="Size.Small" Color="GetRoleColor(role)" Class="mr-1">
@GetRoleDisplayName(role)
</MudChip>
}
}
else
{
<MudText Typo="Typo.caption" Color="Color.Warning">Keine Rolle</MudText>
}
}
</MudTd>
<MudTd DataLabel="Aktionen">
<MudTooltip Text="Rollen verwalten">
<MudIconButton Icon="@Icons.Material.Filled.Security"
Size="Size.Small"
OnClick="@(() => OpenMemberRolesDialog(context))" />
</MudTooltip>
</MudTd>
</RowTemplate>
</MudTable>
}
</MudCardContent>
</MudCard>
</MudTabPanel>
</MudTabs>
}
@ -204,9 +259,11 @@ else
private bool _isLoading = true;
private bool _isLoadingInvitations = true;
private bool _isLoadingPending = true;
private bool _isLoadingMembers = true;
private int _activeTabIndex = 0;
private List<ClubInvitationDto> _invitations = new();
private List<PendingMembershipDto> _pendingMemberships = new();
private List<UserDto> _clubMembers = new();
protected override async Task OnInitializedAsync()
{
@ -217,19 +274,34 @@ else
private async Task LoadAllDataAsync()
{
_isLoading = true;
_isLoadingInvitations = true;
_isLoadingPending = true;
_isLoadingMembers = true;
try
{
var clubId = CurrentClubContext.ClubId;
if (clubId != Guid.Empty)
{
_club = await ClubService.GetByIdAsync(clubId);
await LoadInvitationsAsync();
await LoadPendingMembershipsAsync();
_isLoading = false;
_invitations = (await ClubService.GetInvitationsByClubAsync(clubId)).ToList();
_isLoadingInvitations = false;
_pendingMemberships = (await UserService.GetPendingMembershipsAsync(clubId)).ToList();
_isLoadingPending = false;
_clubMembers = (await UserService.GetUsersByClubAsync(clubId)).ToList();
_isLoadingMembers = false;
}
}
finally
{
_isLoading = false;
_isLoadingInvitations = false;
_isLoadingPending = false;
_isLoadingMembers = false;
}
}
@ -267,6 +339,23 @@ else
}
}
private async Task LoadMembersAsync()
{
_isLoadingMembers = true;
try
{
var clubId = CurrentClubContext.ClubId;
if (clubId != Guid.Empty)
{
_clubMembers = (await UserService.GetUsersByClubAsync(clubId)).ToList();
}
}
finally
{
_isLoadingMembers = false;
}
}
private void ClearError()
{
Dispatcher.Dispatch(new ClearClubErrorAction());
@ -461,4 +550,47 @@ else
}
}
}
private static string GetRoleDisplayName(string role) => role switch
{
UserRole.Viewer => "Betrachter",
UserRole.Editor => "Bearbeiter",
UserRole.Admin => "Administrator",
_ => role
};
private static Color GetRoleColor(string role) => role switch
{
UserRole.Admin => Color.Error,
UserRole.Editor => Color.Warning,
UserRole.Viewer => Color.Info,
_ => Color.Default
};
private async Task OpenMemberRolesDialog(UserDto user)
{
var currentUser = await UserService.GetCurrentUserAsync();
if (currentUser == null)
{
Snackbar.Add("Nicht angemeldet", Severity.Error);
return;
}
var parameters = new DialogParameters
{
{ "User", user },
{ "ClubId", CurrentClubContext.ClubId },
{ "CurrentUserProfileId", currentUser.ProfileId }
};
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small };
var dialog = await DialogService.ShowAsync<ClubMemberRolesDialog>("Rollen verwalten", parameters, options);
var result = await dialog.Result;
if (result != null && !result.Canceled && result.Data is bool hasChanges && hasChanges)
{
await LoadMembersAsync();
StateHasChanged();
}
}
}

View File

@ -1,3 +1,4 @@
using System.Globalization;
using ApexCharts;
using Fluxor;
using Fluxor.Blazor.Web.ReduxDevTools;
@ -140,4 +141,8 @@ await BootstrapSeeder.SeedTriggersAsync(app.Services);
await DemoSeeder.SeedAsync(app.Services, app.Configuration, app.Environment);
var culture = new CultureInfo("de-DE");
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
app.Run();