Compare commits

..

8 Commits

39 changed files with 1399 additions and 526 deletions

View File

@@ -9,8 +9,3 @@
</div> </div>
</div> </div>
@code {
[Parameter] public AccountSummaryDto Account { get; set; } = default!;
private void Navigate() => Navigation.NavigateTo($"/accounts/{Account.Id}");
}

View File

@@ -0,0 +1,20 @@
using Duempelkas.App.Services.Models;
using Microsoft.AspNetCore.Components;
namespace Duempelkas.App.Components.Accounts;
public partial class AccountCard
{
#region Parameters
[Parameter]
public AccountSummaryDto Account { get; set; } = default!;
#endregion
#region Actions
private void Navigate() => Navigation.NavigateTo($"/accounts/{Account.Id}");
#endregion
}

View File

@@ -7,6 +7,3 @@
} }
</div> </div>
@code {
[Parameter] public List<AccountSummaryDto> Accounts { get; set; } = new();
}

View File

@@ -0,0 +1,14 @@
using Duempelkas.App.Services.Models;
using Microsoft.AspNetCore.Components;
namespace Duempelkas.App.Components.Accounts;
public partial class AccountCardList
{
#region Parameters
[Parameter]
public List<AccountSummaryDto> Accounts { get; set; } = new();
#endregion
}

View File

@@ -34,17 +34,3 @@
</td> </td>
</tr> </tr>
@code {
[Parameter] public EntryDto Entry { get; set; } = default!;
[Parameter] public EventCallback<int> OnDelete { get; set; }
[Parameter] public EventCallback<int> OnRestore { get; set; }
[Parameter] public EventCallback<int> OnEdit { get; set; }
private string GetRowClass()
{
var classes = new List<string>();
if (Entry.IsTransfer) classes.Add("entry-row-transfer");
if (Entry.IsDeleted) classes.Add("entry-deleted");
return string.Join(" ", classes);
}
}

View File

@@ -0,0 +1,43 @@
using Duempelkas.App.Services.Models;
using Microsoft.AspNetCore.Components;
namespace Duempelkas.App.Components.Accounts;
public partial class EntryRow
{
#region Parameters
[Parameter]
public EntryDto Entry { get; set; } = default!;
[Parameter]
public EventCallback<int> OnDelete { get; set; }
[Parameter]
public EventCallback<int> OnRestore { get; set; }
[Parameter]
public EventCallback<int> OnEdit { get; set; }
#endregion
#region Helpers
private string GetRowClass()
{
var classes = new List<string>();
if (Entry.IsTransfer)
{
classes.Add("entry-row-transfer");
}
if (Entry.IsDeleted)
{
classes.Add("entry-deleted");
}
return string.Join(" ", classes);
}
#endregion
}

View File

@@ -23,9 +23,3 @@
<div class="text-center py-3 text-muted">Keine Buchungen vorhanden.</div> <div class="text-center py-3 text-muted">Keine Buchungen vorhanden.</div>
} }
@code {
[Parameter] public List<EntryDto> Entries { get; set; } = new();
[Parameter] public EventCallback<int> OnDeleteEntry { get; set; }
[Parameter] public EventCallback<int> OnRestoreEntry { get; set; }
[Parameter] public EventCallback<int> OnEditEntry { get; set; }
}

View File

@@ -0,0 +1,23 @@
using Duempelkas.App.Services.Models;
using Microsoft.AspNetCore.Components;
namespace Duempelkas.App.Components.Accounts;
public partial class EntryTable
{
#region Parameters
[Parameter]
public List<EntryDto> Entries { get; set; } = new();
[Parameter]
public EventCallback<int> OnDeleteEntry { get; set; }
[Parameter]
public EventCallback<int> OnRestoreEntry { get; set; }
[Parameter]
public EventCallback<int> OnEditEntry { get; set; }
#endregion
}

View File

@@ -1,4 +1,4 @@
<div class="dialog-backdrop" @onclick="Cancel"> <div class="dialog-backdrop">
<div class="dialog-content" @onclick:stopPropagation="true"> <div class="dialog-content" @onclick:stopPropagation="true">
<h5 class="mb-3">Neues Konto</h5> <h5 class="mb-3">Neues Konto</h5>
<div class="form-container"> <div class="form-container">
@@ -13,17 +13,3 @@
</div> </div>
</div> </div>
@code {
[Parameter] public EventCallback<string> OnSave { get; set; }
[Parameter] public EventCallback OnCancel { get; set; }
private string name = string.Empty;
private async Task Save()
{
if (!string.IsNullOrWhiteSpace(name))
await OnSave.InvokeAsync(name.Trim());
}
private async Task Cancel() => await OnCancel.InvokeAsync();
}

View File

@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Components;
namespace Duempelkas.App.Components.Dialogs;
public partial class AddAccountDialog
{
#region Parameters
[Parameter]
public EventCallback<string> OnSave { get; set; }
[Parameter]
public EventCallback OnCancel { get; set; }
#endregion
#region Fields
private string name = string.Empty;
#endregion
#region Actions
private async Task Save()
{
if (!string.IsNullOrWhiteSpace(name))
{
await OnSave.InvokeAsync(name.Trim());
}
}
private async Task Cancel() => await OnCancel.InvokeAsync();
#endregion
}

View File

@@ -1,6 +1,6 @@
@inject IEntryService EntryService @inject IEntryService EntryService
<div class="dialog-backdrop" @onclick="Cancel"> <div class="dialog-backdrop">
<div class="dialog-content" @onclick:stopPropagation="true"> <div class="dialog-content" @onclick:stopPropagation="true">
<h5>@(EditEntry != null ? "Buchung bearbeiten" : "Neue Buchung")</h5> <h5>@(EditEntry != null ? "Buchung bearbeiten" : "Neue Buchung")</h5>
@@ -34,36 +34,3 @@
</div> </div>
</div> </div>
@code {
[Parameter] public int AccountId { get; set; }
[Parameter] public EntryDto? EditEntry { get; set; }
[Parameter] public EventCallback OnSave { get; set; }
[Parameter] public EventCallback OnCancel { get; set; }
private EntryType entryType = EntryType.Income;
private DateTime date = DateTime.Today;
private string title = string.Empty;
private decimal amount;
protected override void OnParametersSet()
{
if (EditEntry != null)
{
entryType = EditEntry.Type;
date = EditEntry.Date;
title = EditEntry.Title;
amount = EditEntry.Amount;
}
}
private async Task Save()
{
if (EditEntry != null)
await EntryService.UpdateEntryAsync(EditEntry.Id, date, title.Trim(), amount);
else
await EntryService.CreateEntryAsync(AccountId, entryType, date, title.Trim(), amount);
await OnSave.InvokeAsync();
}
private async Task Cancel() => await OnCancel.InvokeAsync();
}

View File

@@ -0,0 +1,68 @@
using Duempelkas.App.Services.Models;
using Duempelkas.Domain.Enums;
using Microsoft.AspNetCore.Components;
namespace Duempelkas.App.Components.Dialogs;
public partial class AddEntryDialog
{
#region Parameters
[Parameter]
public int AccountId { get; set; }
[Parameter]
public EntryDto? EditEntry { get; set; }
[Parameter]
public EventCallback OnSave { get; set; }
[Parameter]
public EventCallback OnCancel { get; set; }
#endregion
#region Fields
private EntryType entryType = EntryType.Income;
private DateTime date = DateTime.Today;
private string title = string.Empty;
private decimal amount;
#endregion
#region Lifecycle
protected override void OnParametersSet()
{
if (EditEntry != null)
{
entryType = EditEntry.Type;
date = EditEntry.Date;
title = EditEntry.Title;
amount = EditEntry.Amount;
}
}
#endregion
#region Actions
private async Task Save()
{
if (EditEntry != null)
{
await EntryService.UpdateEntryAsync(EditEntry.Id, date, title.Trim(), amount);
}
else
{
await EntryService.CreateEntryAsync(AccountId, entryType, date, title.Trim(), amount);
}
await OnSave.InvokeAsync();
}
private async Task Cancel() => await OnCancel.InvokeAsync();
#endregion
}

View File

@@ -1,7 +1,7 @@
@inject IEntryService EntryService @inject IEntryService EntryService
@inject IAccountService AccountService @inject IAccountService AccountService
<div class="dialog-backdrop" @onclick="Cancel"> <div class="dialog-backdrop">
<div class="dialog-content" @onclick:stopPropagation="true"> <div class="dialog-content" @onclick:stopPropagation="true">
<h5 class="mb-3">@(EditEntry != null ? "Umbuchung bearbeiten" : "Neue Umbuchung")</h5> <h5 class="mb-3">@(EditEntry != null ? "Umbuchung bearbeiten" : "Neue Umbuchung")</h5>
@@ -27,48 +27,9 @@
<div class="d-flex justify-content-end gap-2"> <div class="d-flex justify-content-end gap-2">
<button class="btn btn-success" @onclick="Save" <button class="btn btn-success" @onclick="Save"
disabled="@(!CanSave)"><i class="bi bi-arrow-left-right"></i> @(EditEntry != null ? "Speichern" : "Umbuchen")</button> disabled="@(!CanSave)"><i class="bi bi-arrow-left-right"></i> @(EditEntry != null ? "Speichern" : "Hinzufügen")</button>
<button class="btn btn-outline-secondary" @onclick="Cancel"><i class="bi bi-x-lg"></i> Abbrechen</button> <button class="btn btn-outline-secondary" @onclick="Cancel"><i class="bi bi-x-lg"></i> Abbrechen</button>
</div> </div>
</div> </div>
</div> </div>
@code {
[Parameter] public int SourceAccountId { get; set; }
[Parameter] public EntryDto? EditEntry { get; set; }
[Parameter] public EventCallback OnSave { get; set; }
[Parameter] public EventCallback OnCancel { get; set; }
private List<AccountSummaryDto> accounts = new();
private int targetAccountId;
private DateTime date = DateTime.Today;
private string title = string.Empty;
private decimal amount;
private bool CanSave => targetAccountId > 0 && !string.IsNullOrWhiteSpace(title) && amount > 0;
protected override async Task OnParametersSetAsync()
{
if (!accounts.Any())
accounts = await AccountService.GetAllAccountsAsync();
if (EditEntry != null)
{
targetAccountId = EditEntry.LinkedAccountId ?? 0;
date = EditEntry.Date;
title = EditEntry.Title;
amount = EditEntry.Amount;
}
}
private async Task Save()
{
if (EditEntry != null)
await EntryService.UpdateTransferAsync(EditEntry.Id, targetAccountId, date, title.Trim(), amount);
else
await EntryService.CreateTransferAsync(SourceAccountId, targetAccountId, date, title.Trim(), amount);
await OnSave.InvokeAsync();
}
private async Task Cancel() => await OnCancel.InvokeAsync();
}

View File

@@ -0,0 +1,79 @@
using Duempelkas.App.Services.Models;
using Microsoft.AspNetCore.Components;
namespace Duempelkas.App.Components.Dialogs;
public partial class AddTransferDialog
{
#region Parameters
[Parameter]
public int SourceAccountId { get; set; }
[Parameter]
public EntryDto? EditEntry { get; set; }
[Parameter]
public EventCallback OnSave { get; set; }
[Parameter]
public EventCallback OnCancel { get; set; }
#endregion
#region Fields
private List<AccountSummaryDto> accounts = new();
private int targetAccountId;
private DateTime date = DateTime.Today;
private string title = string.Empty;
private decimal amount;
#endregion
#region Properties
private bool CanSave => targetAccountId > 0 && !string.IsNullOrWhiteSpace(title) && amount > 0;
#endregion
#region Lifecycle
protected override async Task OnParametersSetAsync()
{
if (!accounts.Any())
{
accounts = await AccountService.GetAllAccountsAsync();
}
if (EditEntry != null)
{
targetAccountId = EditEntry.LinkedAccountId ?? 0;
date = EditEntry.Date;
title = EditEntry.Title;
amount = EditEntry.Amount;
}
}
#endregion
#region Actions
private async Task Save()
{
if (EditEntry != null)
{
await EntryService.UpdateTransferAsync(EditEntry.Id, targetAccountId, date, title.Trim(), amount);
}
else
{
await EntryService.CreateTransferAsync(SourceAccountId, targetAccountId, date, title.Trim(), amount);
}
await OnSave.InvokeAsync();
}
private async Task Cancel() => await OnCancel.InvokeAsync();
#endregion
}

View File

@@ -1,4 +1,4 @@
<div class="dialog-backdrop" @onclick="Cancel"> <div class="dialog-backdrop">
<div class="dialog-content" @onclick:stopPropagation="true"> <div class="dialog-content" @onclick:stopPropagation="true">
<h5>@Title</h5> <h5>@Title</h5>
<p>@Message</p> <p>@Message</p>
@@ -9,16 +9,3 @@
</div> </div>
</div> </div>
@code {
[Parameter] public string Title { get; set; } = "Bestätigung";
[Parameter] public string Message { get; set; } = "Sind Sie sicher?";
[Parameter] public string ConfirmText { get; set; } = "Ja, bestätigen";
[Parameter] public string CancelText { get; set; } = "Nein, abbrechen";
[Parameter] public string ConfirmButtonClass { get; set; } = "btn btn-danger";
[Parameter] public string ConfirmIconClass { get; set; } = "bi bi-trash";
[Parameter] public EventCallback OnConfirm { get; set; }
[Parameter] public EventCallback OnCancel { get; set; }
private async Task Confirm() => await OnConfirm.InvokeAsync();
private async Task Cancel() => await OnCancel.InvokeAsync();
}

View File

@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Components;
namespace Duempelkas.App.Components.Dialogs;
public partial class ConfirmDialog
{
#region Parameters
[Parameter]
public string Title { get; set; } = "Bestätigung";
[Parameter]
public string Message { get; set; } = "Sind Sie sicher?";
[Parameter]
public string ConfirmText { get; set; } = "Ja, bestätigen";
[Parameter]
public string CancelText { get; set; } = "Nein, abbrechen";
[Parameter]
public string ConfirmButtonClass { get; set; } = "btn btn-danger";
[Parameter]
public string ConfirmIconClass { get; set; } = "bi bi-trash";
[Parameter]
public EventCallback OnConfirm { get; set; }
[Parameter]
public EventCallback OnCancel { get; set; }
#endregion
#region Actions
private async Task Confirm() => await OnConfirm.InvokeAsync();
private async Task Cancel() => await OnCancel.InvokeAsync();
#endregion
}

View File

@@ -1,4 +1,4 @@
<div class="dialog-backdrop" @onclick="Cancel"> <div class="dialog-backdrop">
<div class="dialog-content" @onclick:stopPropagation="true"> <div class="dialog-content" @onclick:stopPropagation="true">
<h5 class="mb-3">Übertrag bearbeiten</h5> <h5 class="mb-3">Übertrag bearbeiten</h5>
<div class="form-container"> <div class="form-container">
@@ -12,16 +12,3 @@
</div> </div>
</div> </div>
@code {
[Parameter] public decimal CurrentAmount { get; set; }
[Parameter] public EventCallback<decimal> OnSave { get; set; }
[Parameter] public EventCallback OnCancel { get; set; }
private decimal amount;
protected override void OnParametersSet() => amount = CurrentAmount;
private async Task Save() => await OnSave.InvokeAsync(amount);
private async Task Cancel() => await OnCancel.InvokeAsync();
}

View File

@@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Components;
namespace Duempelkas.App.Components.Dialogs;
public partial class EditCarryoverDialog
{
#region Parameters
[Parameter]
public decimal CurrentAmount { get; set; }
[Parameter]
public EventCallback<decimal> OnSave { get; set; }
[Parameter]
public EventCallback OnCancel { get; set; }
#endregion
#region Fields
private decimal amount;
#endregion
#region Lifecycle
protected override void OnParametersSet() => amount = CurrentAmount;
#endregion
#region Actions
private async Task Save() => await OnSave.InvokeAsync(amount);
private async Task Cancel() => await OnCancel.InvokeAsync();
#endregion
}

View File

@@ -1,4 +1,4 @@
<div class="dialog-backdrop" @onclick="Cancel"> <div class="dialog-backdrop">
<div class="dialog-content" @onclick:stopPropagation="true"> <div class="dialog-content" @onclick:stopPropagation="true">
<h5 class="mb-3">@DialogTitle</h5> <h5 class="mb-3">@DialogTitle</h5>
<div class="form-container"> <div class="form-container">
@@ -12,22 +12,3 @@
</div> </div>
</div> </div>
@code {
[Parameter] public string CurrentName { get; set; } = string.Empty;
[Parameter] public string DialogTitle { get; set; } = "Kontoname bearbeiten";
[Parameter] public string NameLabel { get; set; } = "Kontoname";
[Parameter] public EventCallback<string> OnSave { get; set; }
[Parameter] public EventCallback OnCancel { get; set; }
private string name = string.Empty;
protected override void OnParametersSet() => name = CurrentName;
private async Task Save()
{
if (!string.IsNullOrWhiteSpace(name))
await OnSave.InvokeAsync(name.Trim());
}
private async Task Cancel() => await OnCancel.InvokeAsync();
}

View File

@@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Components;
namespace Duempelkas.App.Components.Dialogs;
public partial class EditNameDialog
{
#region Parameters
[Parameter]
public string CurrentName { get; set; } = string.Empty;
[Parameter]
public string DialogTitle { get; set; } = "Kontoname bearbeiten";
[Parameter]
public string NameLabel { get; set; } = "Kontoname";
[Parameter]
public EventCallback<string> OnSave { get; set; }
[Parameter]
public EventCallback OnCancel { get; set; }
#endregion
#region Fields
private string name = string.Empty;
#endregion
#region Lifecycle
protected override void OnParametersSet() => name = CurrentName;
#endregion
#region Actions
private async Task Save()
{
if (!string.IsNullOrWhiteSpace(name))
{
await OnSave.InvokeAsync(name.Trim());
}
}
private async Task Cancel() => await OnCancel.InvokeAsync();
#endregion
}

View File

@@ -1,5 +1,4 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@using System.Reflection
<div class="d-flex flex-column vh-100"> <div class="d-flex flex-column vh-100">
<nav class="app-navbar d-flex justify-content-between align-items-center"> <nav class="app-navbar d-flex justify-content-between align-items-center">
@@ -19,11 +18,11 @@
@if (showAboutDialog) @if (showAboutDialog)
{ {
<div class="dialog-backdrop" @onclick="CloseAboutDialog"> <div class="dialog-backdrop">
<div class="dialog-content" @onclick:stopPropagation="true"> <div class="dialog-content" @onclick:stopPropagation="true">
<h5>Über Dümpelkas &middot; Version @AppVersion</h5> <h5>Über Dümpelkas &middot; Version @AppVersion</h5>
<p class="mb-2">Entwickler: Andre Beging</p> <p class="mb-2">Entwickler: Andre Beging</p>
<p class="mb-3">E-Mail: <a href="mailto:mail@beging.de" style="color: white;">mail@beging.de</a></p> <p class="mb-3">E-Mail: <a href="mailto:mail@beging.de" style="color: var(--color-text);">mail@beging.de</a></p>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<button type="button" class="btn btn-outline-secondary" @onclick="CloseAboutDialog"> <button type="button" class="btn btn-outline-secondary" @onclick="CloseAboutDialog">
<i class="bi bi-x-lg"></i> Schließen <i class="bi bi-x-lg"></i> Schließen
@@ -33,28 +32,3 @@
</div> </div>
} }
@code {
private bool showAboutDialog;
private void OpenAboutDialog() => showAboutDialog = true;
private void CloseAboutDialog() => showAboutDialog = false;
private static string AppVersion
{
get
{
var entryAssembly = Assembly.GetEntryAssembly();
var infoVersion = entryAssembly?
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion;
if (!string.IsNullOrWhiteSpace(infoVersion))
{
return infoVersion.Split('+')[0];
}
return entryAssembly?.GetName().Version?.ToString(2) ?? "1.0";
}
}
}

View File

@@ -0,0 +1,42 @@
using System.Reflection;
namespace Duempelkas.App.Components.Layout;
public partial class MainLayout
{
#region Fields
private bool showAboutDialog;
#endregion
#region Properties
private static string AppVersion
{
get
{
var entryAssembly = Assembly.GetEntryAssembly();
var infoVersion = entryAssembly?
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion;
if (!string.IsNullOrWhiteSpace(infoVersion))
{
return infoVersion.Split('+')[0];
}
return entryAssembly?.GetName().Version?.ToString(2) ?? "1.0";
}
}
#endregion
#region Event Handlers
private void OpenAboutDialog() => showAboutDialog = true;
private void CloseAboutDialog() => showAboutDialog = false;
#endregion
}

View File

@@ -39,8 +39,8 @@
<i class="bi bi-file-earmark-pdf"></i> PDF <i class="bi bi-file-earmark-pdf"></i> PDF
</button> </button>
| |
<button class="btn btn-nav @(showCurrentYearOnly ? "btn-primary" : "btn-outline-secondary")" @onclick="ToggleYearFilter"> <button class="btn btn-nav btn-primary" @onclick="OpenYearFilterDialog">
<i class="bi bi-funnel"></i> @(showCurrentYearOnly ? $"Nur {DateTime.Now.Year}" : "Alle Buchungen") <i class="bi bi-funnel"></i> Ansicht<br>@GetFilterLabel()
</button> </button>
</div> </div>
@@ -56,21 +56,21 @@
</div> </div>
<div class="text-end"> <div class="text-end">
<small class="text-muted">Einnahmen</small> <small class="text-muted">Einnahmen</small>
<div class="amount-positive">@FormatAmount(showCurrentYearOnly ? balance.CurrentYearIncome : balance.TotalIncome)</div> <div class="amount-positive">@FormatAmount(selectedYear.HasValue ? balance.CurrentYearIncome : balance.TotalIncome)</div>
</div> </div>
<div class="text-end"> <div class="text-end">
<small class="text-muted">Ausgaben</small> <small class="text-muted">Ausgaben</small>
<div class="amount-negative">@FormatAmount(showCurrentYearOnly ? balance.CurrentYearExpense : balance.TotalExpense)</div> <div class="amount-negative">@FormatAmount(selectedYear.HasValue ? balance.CurrentYearExpense : balance.TotalExpense)</div>
</div> </div>
</div> </div>
</div> </div>
@if (showCurrentYearOnly) @if (selectedYear.HasValue)
{ {
<div class="summary-section"> <div class="summary-section">
<div class="summary-flex"> <div class="summary-flex">
<div> <div>
<small class="text-muted">Übertrag von @(DateTime.Now.Year - 1)</small> <small class="text-muted">Übertrag von @(selectedYear.Value - 1)</small>
<div class="d-flex align-items-center gap-1"> <div class="d-flex align-items-center gap-1">
<span class="fw-bold">@FormatAmount(balance.CarryoverBalance)</span> <span class="fw-bold">@FormatAmount(balance.CarryoverBalance)</span>
<button class="btn-edit-pen" @onclick="() => showEditCarryover = true" title="Übertrag bearbeiten"> <button class="btn-edit-pen" @onclick="() => showEditCarryover = true" title="Übertrag bearbeiten">
@@ -79,7 +79,7 @@
</div> </div>
</div> </div>
<div class="text-end"> <div class="text-end">
<small class="text-muted">Umsätze @DateTime.Now.Year</small> <small class="text-muted">Umsätze @selectedYear.Value</small>
<div class="fw-bold @(balance.CurrentYearIncome - balance.CurrentYearExpense >= 0 ? "amount-positive" : "amount-negative")"> <div class="fw-bold @(balance.CurrentYearIncome - balance.CurrentYearExpense >= 0 ? "amount-positive" : "amount-negative")">
@FormatAmount(balance.CurrentYearIncome - balance.CurrentYearExpense) @FormatAmount(balance.CurrentYearIncome - balance.CurrentYearExpense)
</div> </div>
@@ -179,156 +179,29 @@
OnCancel="() => editingTransferEntry = null" /> OnCancel="() => editingTransferEntry = null" />
} }
@code { @if (showYearFilterDialog)
[Parameter] public int AccountId { get; set; } {
<div class="dialog-backdrop" @onclick="CloseYearFilterDialog">
private AccountSummaryDto? account; <div class="dialog-content" @onclick:stopPropagation="true">
private AccountBalanceDto? balance; <h5>Filter auswählen</h5>
private List<EntryDto>? entries; <p class="text-muted mb-3">Wähle ein Jahr oder alle Buchungen.</p>
private bool showAddEntry, showAddTransfer; <div class="filter-options-grid">
private bool showEditName, showEditCarryover; <button class="btn @(selectedYear.HasValue ? "btn-outline-secondary" : "btn-primary")" @onclick="() => SelectFilterYear(null)">
private bool showCurrentYearOnly = true; Alle Buchungen
</button>
private int? confirmDeleteEntryId; @foreach (var year in availableYears)
private string? confirmDeleteEntryTitle; {
<button class="btn @(selectedYear == year ? "btn-primary" : "btn-outline-secondary")" @onclick="() => SelectFilterYear(year)">
private int? confirmRestoreEntryId; @year
private string? confirmRestoreEntryTitle; </button>
}
private string? savedPdfPath; </div>
<div class="d-flex justify-content-end mt-3">
private EntryDto? editingEntry; <button class="btn btn-outline-secondary" @onclick="CloseYearFilterDialog">
private EntryDto? editingTransferEntry; <i class="bi bi-x-lg"></i> Schließen
</button>
private void NavigateBack() => Navigation.NavigateTo("/"); </div>
</div>
protected override async Task OnParametersSetAsync() </div>
{
await LoadAll();
}
private async Task LoadAll()
{
account = await AccountService.GetAccountAsync(AccountId);
balance = await BalanceQueryService.GetAccountBalanceAsync(AccountId);
entries = await EntryService.GetEntriesAsync(AccountId, showCurrentYearOnly);
}
private async Task ToggleYearFilter()
{
showCurrentYearOnly = !showCurrentYearOnly;
entries = await EntryService.GetEntriesAsync(AccountId, showCurrentYearOnly);
}
private async Task HandleSaveName(string newName)
{
await AccountService.RenameAccountAsync(AccountId, newName);
showEditName = false;
await LoadAll();
}
private async Task HandleSaveCarryover(decimal newAmount)
{
await AccountService.UpdateCarryoverAsync(AccountId, newAmount);
showEditCarryover = false;
await LoadAll();
}
private async Task HandleEntryCreated()
{
showAddEntry = false;
await LoadAll();
}
private async Task HandleTransferCreated()
{
showAddTransfer = false;
await LoadAll();
}
private void RequestDeleteEntry(int entryId)
{
confirmDeleteEntryId = entryId;
confirmDeleteEntryTitle = entries?.FirstOrDefault(e => e.Id == entryId)?.Title;
}
private void CancelDeleteConfirm()
{
confirmDeleteEntryId = null;
confirmDeleteEntryTitle = null;
}
private async Task HandleConfirmDelete()
{
if (confirmDeleteEntryId.HasValue)
{
await EntryService.DeleteEntryAsync(confirmDeleteEntryId.Value);
confirmDeleteEntryId = null;
confirmDeleteEntryTitle = null;
await LoadAll();
}
}
private void RequestRestoreEntry(int entryId)
{
confirmRestoreEntryId = entryId;
confirmRestoreEntryTitle = entries?.FirstOrDefault(e => e.Id == entryId)?.Title;
}
private void CancelRestoreConfirm()
{
confirmRestoreEntryId = null;
confirmRestoreEntryTitle = null;
}
private async Task HandleConfirmRestore()
{
if (confirmRestoreEntryId.HasValue)
{
await EntryService.RestoreEntryAsync(confirmRestoreEntryId.Value);
confirmRestoreEntryId = null;
confirmRestoreEntryTitle = null;
await LoadAll();
}
}
private void RequestEditEntry(int entryId)
{
var entry = entries?.FirstOrDefault(e => e.Id == entryId);
if (entry?.IsTransfer == true)
editingTransferEntry = entry;
else
editingEntry = entry;
}
private async Task HandleEntryEdited()
{
editingEntry = null;
editingTransferEntry = null;
await LoadAll();
}
private async Task HandleExport()
{
var pdf = await PdfStatementService.GenerateStatementAsync(AccountId, showCurrentYearOnly);
var suffix = showCurrentYearOnly ? $"_{DateTime.Now.Year}" : "_Gesamt";
savedPdfPath = await FileSaveService.SaveFileAsync(pdf, $"{account?.Name}{suffix}.pdf");
}
private async Task HandleOpenSavedPdf()
{
if (!string.IsNullOrWhiteSpace(savedPdfPath))
{
await FileSaveService.OpenFileAsync(savedPdfPath);
}
savedPdfPath = null;
}
private void CancelOpenSavedPdf()
{
savedPdfPath = null;
}
private static string FormatAmount(decimal amount) => $"{amount:N2} €";
} }

View File

@@ -0,0 +1,210 @@
using Duempelkas.App.Services.Models;
using Microsoft.AspNetCore.Components;
namespace Duempelkas.App.Pages.Accounts;
public partial class AccountDetail
{
#region Parameters
[Parameter]
public int AccountId { get; set; }
#endregion
#region Fields
private AccountSummaryDto? account;
private AccountBalanceDto? balance;
private List<EntryDto>? entries;
private bool showAddEntry;
private bool showAddTransfer;
private bool showEditName;
private bool showEditCarryover;
private bool showYearFilterDialog;
private int? selectedYear = DateTime.Now.Year;
private List<int> availableYears = [];
private int? confirmDeleteEntryId;
private string? confirmDeleteEntryTitle;
private int? confirmRestoreEntryId;
private string? confirmRestoreEntryTitle;
private string? savedPdfPath;
private EntryDto? editingEntry;
private EntryDto? editingTransferEntry;
#endregion
#region Navigation
private void NavigateBack() => Navigation.NavigateTo("/");
#endregion
#region Lifecycle
protected override async Task OnParametersSetAsync()
{
await LoadAll();
}
#endregion
#region Data Loading
private async Task LoadAll()
{
account = await AccountService.GetAccountAsync(AccountId);
balance = await BalanceQueryService.GetAccountBalanceAsync(AccountId, selectedYear);
entries = await EntryService.GetEntriesAsync(AccountId, selectedYear);
availableYears = await EntryService.GetEntryYearsAsync(AccountId);
}
private void OpenYearFilterDialog()
{
showYearFilterDialog = true;
}
private void CloseYearFilterDialog()
{
showYearFilterDialog = false;
}
private async Task SelectFilterYear(int? year)
{
selectedYear = year;
showYearFilterDialog = false;
await LoadAll();
}
#endregion
#region Actions
private async Task HandleSaveName(string newName)
{
await AccountService.RenameAccountAsync(AccountId, newName);
showEditName = false;
await LoadAll();
}
private async Task HandleSaveCarryover(decimal newAmount)
{
await AccountService.UpdateCarryoverAsync(AccountId, newAmount, selectedYear);
showEditCarryover = false;
await LoadAll();
}
private async Task HandleEntryCreated()
{
showAddEntry = false;
await LoadAll();
}
private async Task HandleTransferCreated()
{
showAddTransfer = false;
await LoadAll();
}
private void RequestDeleteEntry(int entryId)
{
confirmDeleteEntryId = entryId;
confirmDeleteEntryTitle = entries?.FirstOrDefault(e => e.Id == entryId)?.Title;
}
private void CancelDeleteConfirm()
{
confirmDeleteEntryId = null;
confirmDeleteEntryTitle = null;
}
private async Task HandleConfirmDelete()
{
if (confirmDeleteEntryId.HasValue)
{
await EntryService.DeleteEntryAsync(confirmDeleteEntryId.Value);
confirmDeleteEntryId = null;
confirmDeleteEntryTitle = null;
await LoadAll();
}
}
private void RequestRestoreEntry(int entryId)
{
confirmRestoreEntryId = entryId;
confirmRestoreEntryTitle = entries?.FirstOrDefault(e => e.Id == entryId)?.Title;
}
private void CancelRestoreConfirm()
{
confirmRestoreEntryId = null;
confirmRestoreEntryTitle = null;
}
private async Task HandleConfirmRestore()
{
if (confirmRestoreEntryId.HasValue)
{
await EntryService.RestoreEntryAsync(confirmRestoreEntryId.Value);
confirmRestoreEntryId = null;
confirmRestoreEntryTitle = null;
await LoadAll();
}
}
private void RequestEditEntry(int entryId)
{
var entry = entries?.FirstOrDefault(e => e.Id == entryId);
if (entry?.IsTransfer == true)
{
editingTransferEntry = entry;
}
else
{
editingEntry = entry;
}
}
private async Task HandleEntryEdited()
{
editingEntry = null;
editingTransferEntry = null;
await LoadAll();
}
private async Task HandleExport()
{
var pdf = await PdfStatementService.GenerateStatementAsync(AccountId, selectedYear);
var suffix = selectedYear.HasValue ? $"_{selectedYear.Value}" : "_Gesamt";
savedPdfPath = await FileSaveService.SaveFileAsync(pdf, $"{account?.Name}{suffix}.pdf");
}
private async Task HandleOpenSavedPdf()
{
if (!string.IsNullOrWhiteSpace(savedPdfPath))
{
await FileSaveService.OpenFileAsync(savedPdfPath);
}
savedPdfPath = null;
}
private void CancelOpenSavedPdf()
{
savedPdfPath = null;
}
#endregion
#region Helpers
private static string FormatAmount(decimal amount) => $"{amount:N2} €";
private string GetFilterLabel() => selectedYear?.ToString() ?? "Alle";
#endregion
}

View File

@@ -4,9 +4,8 @@
@inject ISettingsService SettingsService @inject ISettingsService SettingsService
@inject IPdfStatementService PdfStatementService @inject IPdfStatementService PdfStatementService
@inject IFileSaveService FileSaveService @inject IFileSaveService FileSaveService
@inject IJSRuntime JsRuntime @inject IEntryService EntryService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@using System.Globalization
<div class="container-fluid"> <div class="container-fluid">
@@ -36,6 +35,13 @@
<button class="btn-nav btn-dark" @onclick="HandleDashboardExportAsync"> <button class="btn-nav btn-dark" @onclick="HandleDashboardExportAsync">
<i class="bi bi-file-earmark-pdf"></i> PDF <i class="bi bi-file-earmark-pdf"></i> PDF
</button> </button>
|
<button class="btn btn-nav btn-primary" @onclick="OpenYearFilterDialog">
<i class="bi bi-funnel"></i> Ansicht<br>@GetFilterLabel()
</button>
<button class="btn btn-nav btn-secondary" @onclick="ToggleThemeAsync" title="Farbmodus wechseln">
<i class="@ThemeButtonIconClass"></i> Modus<br>@ThemeButtonLabel
</button>
</div> </div>
@if (accounts != null && accounts.Any()) @if (accounts != null && accounts.Any())
@@ -97,105 +103,41 @@
OnCancel="CancelOpenSavedPdf" /> OnCancel="CancelOpenSavedPdf" />
} }
@code { @if (showRestoreConfirm)
private List<AccountSummaryDto>? accounts; {
private bool showAddAccount; <ConfirmDialog Title="Restore bestätigen"
private bool showEditClubName; Message="Restore überschreibt die aktuelle Datenbank. Möchten Sie fortfahren?"
private string clubName = string.Empty; ConfirmText="Ja, wiederherstellen"
private string? operationMessage; CancelText="Nein, abbrechen"
private string operationMessageClass = "alert-info"; ConfirmButtonClass="btn btn-warning"
private string? savedPdfPath; ConfirmIconClass="bi bi-arrow-counterclockwise"
private string DisplayClubName => string.IsNullOrWhiteSpace(clubName) ? "Mein Verein" : clubName; OnConfirm="HandleConfirmRestore"
private decimal TotalClubBalance => accounts?.Sum(a => a.TotalBalance) ?? 0m; OnCancel="CancelRestoreConfirm" />
protected override async Task OnInitializedAsync()
{
await LoadClubName();
await LoadAccounts();
}
private async Task LoadAccounts()
{
accounts = await AccountService.GetAllAccountsAsync();
}
private async Task LoadClubName()
{
clubName = await SettingsService.GetClubNameAsync() ?? string.Empty;
}
private async Task HandleAccountCreated(string name)
{
await AccountService.CreateAccountAsync(name);
showAddAccount = false;
await LoadAccounts();
}
private async Task HandleSaveClubName(string newName)
{
await SettingsService.SetClubNameAsync(newName);
showEditClubName = false;
await LoadClubName();
}
private async Task HandleBackupAsync()
{
var message = await BackupService.CreateBackupAsync();
if(message.Contains("fehlgeschlagen", StringComparison.OrdinalIgnoreCase)) {
SetOperationMessage(message, false);
}
}
private async Task HandleRestoreAsync()
{
var confirmed = await JsRuntime.InvokeAsync<bool>("confirm",
"Restore überschreibt die aktuelle Datenbank. Möchten Sie fortfahren?");
if (!confirmed) return;
var message = await BackupService.RestoreBackupAsync();
var isSuccess = message.StartsWith("Wiederherstellung erfolgreich", StringComparison.OrdinalIgnoreCase);
if (isSuccess)
{
// Reload the Blazor app so all components/services re-query from restored DB.
NavigationManager.NavigateTo("/", forceLoad: true);
} else {
SetOperationMessage(message, isSuccess);
}
}
private async Task HandleDashboardExportAsync()
{
var pdf = await PdfStatementService.GenerateDashboardStatementAsync();
savedPdfPath = await FileSaveService.SaveFileAsync(pdf, $"{DisplayClubName}_Übersicht.pdf");
}
private async Task HandleOpenSavedPdf()
{
if (!string.IsNullOrWhiteSpace(savedPdfPath))
{
await FileSaveService.OpenFileAsync(savedPdfPath);
}
savedPdfPath = null;
}
private void CancelOpenSavedPdf()
{
savedPdfPath = null;
}
private void SetOperationMessage(string message, bool success)
{
operationMessage = message;
operationMessageClass = success ? "alert-success" : "alert-danger";
}
private static string FormatCurrency(decimal amount)
{
return amount.ToString("N2", CultureInfo.GetCultureInfo("de-DE")) + " €";
}
} }
@if (showYearFilterDialog)
{
<div class="dialog-backdrop" @onclick="CloseYearFilterDialog">
<div class="dialog-content" @onclick:stopPropagation="true">
<h5>Filter auswählen</h5>
<p class="text-muted mb-3">Wähle ein Jahr oder alle Buchungen.</p>
<div class="filter-options-grid">
<button class="btn @(selectedYear.HasValue ? "btn-outline-secondary" : "btn-primary")" @onclick="() => SelectFilterYear(null)">
Alle Buchungen
</button>
@foreach (var year in availableYears)
{
<button class="btn @(selectedYear == year ? "btn-primary" : "btn-outline-secondary")" @onclick="() => SelectFilterYear(year)">
@year
</button>
}
</div>
<div class="d-flex justify-content-end mt-3">
<button class="btn btn-outline-secondary" @onclick="CloseYearFilterDialog">
<i class="bi bi-x-lg"></i> Schließen
</button>
</div>
</div>
</div>
}

View File

@@ -0,0 +1,213 @@
using System.Globalization;
using Duempelkas.App.Services.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace Duempelkas.App.Pages;
public partial class Dashboard
{
#region Fields
private List<AccountSummaryDto>? accounts;
private bool showAddAccount;
private bool showEditClubName;
private bool showRestoreConfirm;
private bool showYearFilterDialog;
private int? selectedYear = DateTime.Now.Year;
private List<int> availableYears = [];
private string clubName = string.Empty;
private string? operationMessage;
private string operationMessageClass = "alert-info";
private string? savedPdfPath;
private string currentTheme = "light";
#endregion
#region Properties
private string DisplayClubName => string.IsNullOrWhiteSpace(clubName) ? "Mein Verein" : clubName;
private decimal TotalClubBalance => accounts?.Sum(a => a.TotalBalance) ?? 0m;
#endregion
#region Lifecycle
protected override async Task OnInitializedAsync()
{
await LoadClubName();
await LoadAll();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
{
return;
}
try
{
currentTheme = await JS.InvokeAsync<string>("duempelkasThemeGetCurrentTheme");
}
catch (JSException)
{
// Keep default light mode when JS interop is not ready or unavailable.
currentTheme = "light";
}
StateHasChanged();
}
#endregion
#region Data Loading
private async Task LoadAll()
{
await LoadAccounts();
availableYears = await EntryService.GetAllEntryYearsAsync();
}
private async Task LoadAccounts()
{
accounts = await AccountService.GetAllAccountsAsync(selectedYear);
}
private async Task LoadClubName()
{
clubName = await SettingsService.GetClubNameAsync() ?? string.Empty;
}
#endregion
#region Actions
private async Task HandleAccountCreated(string name)
{
await AccountService.CreateAccountAsync(name);
showAddAccount = false;
await LoadAll();
}
private async Task HandleSaveClubName(string newName)
{
await SettingsService.SetClubNameAsync(newName);
showEditClubName = false;
await LoadClubName();
}
private async Task HandleBackupAsync()
{
var message = await BackupService.CreateBackupAsync();
if (message.Contains("fehlgeschlagen", StringComparison.OrdinalIgnoreCase))
{
SetOperationMessage(message, false);
}
}
private Task HandleRestoreAsync()
{
showRestoreConfirm = true;
return Task.CompletedTask;
}
private void CancelRestoreConfirm()
{
showRestoreConfirm = false;
}
private async Task HandleConfirmRestore()
{
showRestoreConfirm = false;
var message = await BackupService.RestoreBackupAsync();
var isSuccess = message.StartsWith("Wiederherstellung erfolgreich", StringComparison.OrdinalIgnoreCase);
if (isSuccess)
{
NavigationManager.NavigateTo("/", forceLoad: true);
}
else
{
SetOperationMessage(message, isSuccess);
}
}
private async Task HandleDashboardExportAsync()
{
var pdf = await PdfStatementService.GenerateDashboardStatementAsync(selectedYear);
var suffix = selectedYear.HasValue ? $"_{selectedYear.Value}" : "_Gesamt";
savedPdfPath = await FileSaveService.SaveFileAsync(pdf, $"{DisplayClubName}_Übersicht{suffix}.pdf");
}
private void OpenYearFilterDialog()
{
showYearFilterDialog = true;
}
private void CloseYearFilterDialog()
{
showYearFilterDialog = false;
}
private async Task SelectFilterYear(int? year)
{
selectedYear = year;
showYearFilterDialog = false;
await LoadAll();
}
private async Task HandleOpenSavedPdf()
{
if (!string.IsNullOrWhiteSpace(savedPdfPath))
{
await FileSaveService.OpenFileAsync(savedPdfPath);
}
savedPdfPath = null;
}
private void CancelOpenSavedPdf()
{
savedPdfPath = null;
}
#endregion
#region Helpers
private void SetOperationMessage(string message, bool success)
{
operationMessage = message;
operationMessageClass = success ? "alert-success" : "alert-danger";
}
private static string FormatCurrency(decimal amount)
{
return amount.ToString("N2", CultureInfo.GetCultureInfo("de-DE")) + " €";
}
private string GetFilterLabel() => selectedYear?.ToString() ?? "Alle";
private string ThemeButtonLabel => currentTheme == "dark" ? "Hell" : "Dunkel";
private string ThemeButtonIconClass => currentTheme == "dark" ? "bi bi-sun" : "bi bi-moon-stars";
private async Task ToggleThemeAsync()
{
try
{
currentTheme = await JS.InvokeAsync<string>("duempelkasThemeToggleTheme");
}
catch (JSException)
{
currentTheme = currentTheme == "dark" ? "light" : "dark";
}
}
[Inject]
private IJSRuntime JS { get; set; } = default!;
#endregion
}

View File

@@ -29,22 +29,3 @@
</div> </div>
</div> </div>
@code {
private string clubName = string.Empty;
private bool saving;
private bool saved;
protected override async Task OnInitializedAsync()
{
clubName = await SettingsService.GetClubNameAsync() ?? string.Empty;
}
private async Task Save()
{
saving = true;
saved = false;
await SettingsService.SetClubNameAsync(clubName);
saved = true;
saving = false;
}
}

View File

@@ -0,0 +1,34 @@
namespace Duempelkas.App.Pages;
public partial class Settings
{
#region Fields
private string clubName = string.Empty;
private bool saving;
private bool saved;
#endregion
#region Lifecycle
protected override async Task OnInitializedAsync()
{
clubName = await SettingsService.GetClubNameAsync() ?? string.Empty;
}
#endregion
#region Actions
private async Task Save()
{
saving = true;
saved = false;
await SettingsService.SetClubNameAsync(clubName);
saved = true;
saving = false;
}
#endregion
}

View File

@@ -5,9 +5,11 @@ namespace Duempelkas.App.Services;
public interface IAccountService public interface IAccountService
{ {
Task<List<AccountSummaryDto>> GetAllAccountsAsync(); Task<List<AccountSummaryDto>> GetAllAccountsAsync();
Task<List<AccountSummaryDto>> GetAllAccountsAsync(int? year);
Task<AccountSummaryDto> GetAccountAsync(int accountId); Task<AccountSummaryDto> GetAccountAsync(int accountId);
Task<AccountSummaryDto> CreateAccountAsync(string name); Task<AccountSummaryDto> CreateAccountAsync(string name);
Task RenameAccountAsync(int accountId, string newName); Task RenameAccountAsync(int accountId, string newName);
Task UpdateCarryoverAsync(int accountId, decimal carryoverBalance); Task UpdateCarryoverAsync(int accountId, decimal carryoverBalance);
Task UpdateCarryoverAsync(int accountId, decimal carryoverBalance, int? year);
Task DeleteAccountAsync(int accountId); Task DeleteAccountAsync(int accountId);
} }

View File

@@ -4,5 +4,5 @@ namespace Duempelkas.App.Services;
public interface IBalanceQueryService public interface IBalanceQueryService
{ {
Task<AccountBalanceDto> GetAccountBalanceAsync(int accountId); Task<AccountBalanceDto> GetAccountBalanceAsync(int accountId, int? year = null);
} }

View File

@@ -5,7 +5,10 @@ namespace Duempelkas.App.Services;
public interface IEntryService public interface IEntryService
{ {
Task<List<EntryDto>> GetEntriesAsync(int accountId, int? year);
Task<List<EntryDto>> GetEntriesAsync(int accountId, bool currentYearOnly); Task<List<EntryDto>> GetEntriesAsync(int accountId, bool currentYearOnly);
Task<List<int>> GetEntryYearsAsync(int accountId);
Task<List<int>> GetAllEntryYearsAsync();
Task<EntryDto> CreateEntryAsync(int accountId, EntryType type, DateTime date, string title, decimal amount); Task<EntryDto> CreateEntryAsync(int accountId, EntryType type, DateTime date, string title, decimal amount);
Task CreateTransferAsync(int sourceAccountId, int targetAccountId, DateTime date, string title, decimal amount); Task CreateTransferAsync(int sourceAccountId, int targetAccountId, DateTime date, string title, decimal amount);
Task DeleteEntryAsync(int entryId); Task DeleteEntryAsync(int entryId);

View File

@@ -2,6 +2,8 @@ namespace Duempelkas.App.Services;
public interface IPdfStatementService public interface IPdfStatementService
{ {
Task<byte[]> GenerateStatementAsync(int accountId, int? year);
Task<byte[]> GenerateStatementAsync(int accountId, bool currentYearOnly); Task<byte[]> GenerateStatementAsync(int accountId, bool currentYearOnly);
Task<byte[]> GenerateDashboardStatementAsync(); Task<byte[]> GenerateDashboardStatementAsync();
Task<byte[]> GenerateDashboardStatementAsync(int? year);
} }

View File

@@ -1,9 +1,19 @@
/* Duempelkas App Styles Dark Mode */ /* Duempelkas App Styles */
:root { :root {
--color-income: #4ade80; --color-income: #4ade80;
--color-expense: #f87171; --color-expense: #f87171;
--color-transfer: #60a5fa; --color-transfer: #60a5fa;
--color-bg: #ddd;
--color-surface: #ffffff;
--color-surface-hover: #f1f5f9;
--color-border: #d9e2ef;
--color-text: #0f172a;
--color-text-muted: #64748b;
--color-accent: #4f46e5;
}
html[data-ui-theme="dark"] {
--color-bg: #1a1a2e; --color-bg: #1a1a2e;
--color-surface: #16213e; --color-surface: #16213e;
--color-surface-hover: #1e2d4a; --color-surface-hover: #1e2d4a;
@@ -138,6 +148,16 @@ html, body {
margin-bottom: 0.5rem !important; margin-bottom: 0.5rem !important;
} }
.filter-options-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 0.5rem;
}
.filter-options-grid .btn {
justify-content: center;
}
/* Navbar */ /* Navbar */
.app-navbar { .app-navbar {
background-color: var(--color-surface); background-color: var(--color-surface);
@@ -301,7 +321,7 @@ html, body {
.btn-dark { .btn-dark {
background-color: #334155; background-color: #334155;
border-color: #475569; border-color: #475569;
color: var(--color-text); color: #f8fafc;
} }
.btn-dark:hover { .btn-dark:hover {
@@ -309,6 +329,12 @@ html, body {
border-color: #64748b; border-color: #64748b;
} }
html[data-ui-theme="light"] .btn-dark {
background-color: #334155;
border-color: #334155;
color: #f8fafc;
}
/* Table */ /* Table */
.table { .table {
--bs-table-bg: transparent; --bs-table-bg: transparent;

View File

@@ -1,9 +1,113 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de" data-bs-theme="dark"> <html lang="de">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" /> <base href="/" />
<script>
(function () {
function getCookie(name) {
var parts = document.cookie ? document.cookie.split(';') : [];
for (var i = 0; i < parts.length; i++) {
var part = parts[i].trim();
if (part.indexOf(name + '=') === 0) {
return decodeURIComponent(part.substring(name.length + 1));
}
}
return null;
}
function setCookie(name, value) {
document.cookie = name + '=' + encodeURIComponent(value) + '; path=/; max-age=31536000; samesite=lax';
}
function getStoredTheme() {
try {
var fromStorage = localStorage.getItem('duempelkas-theme');
if (fromStorage === 'light' || fromStorage === 'dark') {
return fromStorage;
}
} catch (_) {
// Ignore storage access issues and fall back to cookies/system.
}
var fromCookie = getCookie('duempelkas-theme');
if (fromCookie === 'light' || fromCookie === 'dark') {
return fromCookie;
}
return null;
}
function detectSystemTheme() {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
function normalizeTheme(value) {
return value === 'dark' ? 'dark' : 'light';
}
function applyTheme(theme, persist) {
var resolved = normalizeTheme(theme);
document.documentElement.setAttribute('data-ui-theme', resolved);
document.documentElement.setAttribute('data-bs-theme', resolved);
if (document.body) {
document.body.setAttribute('data-ui-theme', resolved);
document.body.setAttribute('data-bs-theme', resolved);
}
if (persist) {
try {
localStorage.setItem('duempelkas-theme', resolved);
} catch (_) {
// Ignore storage access issues and keep cookie as fallback.
}
setCookie('duempelkas-theme', resolved);
}
return resolved;
}
var initialTheme = getStoredTheme() || detectSystemTheme() || 'light';
applyTheme(initialTheme, false);
var duempelkasThemeGetCurrentTheme = function () {
var current = document.documentElement.getAttribute('data-ui-theme');
return normalizeTheme(current);
};
var duempelkasThemeSetTheme = function (theme) {
return applyTheme(theme, true);
};
var duempelkasThemeToggleTheme = function () {
var current = document.documentElement.getAttribute('data-ui-theme');
var next = current === 'dark' ? 'light' : 'dark';
return applyTheme(next, true);
};
globalThis.duempelkasThemeGetCurrentTheme = duempelkasThemeGetCurrentTheme;
globalThis.duempelkasThemeSetTheme = duempelkasThemeSetTheme;
globalThis.duempelkasThemeToggleTheme = duempelkasThemeToggleTheme;
window.duempelkasThemeGetCurrentTheme = duempelkasThemeGetCurrentTheme;
window.duempelkasThemeSetTheme = duempelkasThemeSetTheme;
window.duempelkasThemeToggleTheme = duempelkasThemeToggleTheme;
// Keep object-style API for compatibility with older calls.
window.duempelkasTheme = {
getCurrentTheme: duempelkasThemeGetCurrentTheme,
setTheme: duempelkasThemeSetTheme,
toggleTheme: duempelkasThemeToggleTheme
};
})();
</script>
<title>Dümpelkas Kassenbuch</title> <title>Dümpelkas Kassenbuch</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet" rel="stylesheet"

View File

@@ -12,6 +12,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="QuestPDF" Version="*" /> <PackageReference Include="QuestPDF" Version="*" />
<PackageReference Include="SkiaSharp" Version="2.88.9" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.88.9" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -13,16 +13,60 @@ public class AccountService : IAccountService
public AccountService(IDbContextFactory<FinanceDbContext> dbFactory) => _dbFactory = dbFactory; public AccountService(IDbContextFactory<FinanceDbContext> dbFactory) => _dbFactory = dbFactory;
public async Task<List<AccountSummaryDto>> GetAllAccountsAsync() public Task<List<AccountSummaryDto>> GetAllAccountsAsync()
{
return GetAllAccountsAsync(null);
}
public async Task<List<AccountSummaryDto>> GetAllAccountsAsync(int? year)
{ {
await using var db = await _dbFactory.CreateDbContextAsync(); await using var db = await _dbFactory.CreateDbContextAsync();
var accounts = await db.Accounts var accounts = await db.Accounts
.Include(a => a.Entries)
.OrderBy(a => a.Name) .OrderBy(a => a.Name)
.ToListAsync(); .ToListAsync();
return accounts.Select(MapToSummary).ToList(); var entries = db.Entries.Where(e => !e.IsDeleted);
if (year.HasValue)
{
var movementBeforeYearByAccountId = await entries
.Where(e => e.Date.Year < year.Value)
.GroupBy(e => e.AccountId)
.ToDictionaryAsync(
g => g.Key,
g => g.Sum(e => e.Type == EntryType.Income ? e.Amount : -e.Amount));
var movementInYearByAccountId = await entries
.Where(e => e.Date.Year == year.Value)
.GroupBy(e => e.AccountId)
.ToDictionaryAsync(
g => g.Key,
g => g.Sum(e => e.Type == EntryType.Income ? e.Amount : -e.Amount));
return accounts
.Select(account =>
{
var carryoverForYear = account.CarryoverBalance + movementBeforeYearByAccountId.GetValueOrDefault(account.Id);
var totalBalance = carryoverForYear + movementInYearByAccountId.GetValueOrDefault(account.Id);
return new AccountSummaryDto(account.Id, account.Name, carryoverForYear, totalBalance, account.CreatedUtc);
})
.ToList();
}
var movementAllByAccountId = await entries
.GroupBy(e => e.AccountId)
.ToDictionaryAsync(
g => g.Key,
g => g.Sum(e => e.Type == EntryType.Income ? e.Amount : -e.Amount));
return accounts
.Select(account =>
{
var totalBalance = account.CarryoverBalance + movementAllByAccountId.GetValueOrDefault(account.Id);
return new AccountSummaryDto(account.Id, account.Name, account.CarryoverBalance, totalBalance, account.CreatedUtc);
})
.ToList();
} }
public async Task<AccountSummaryDto> GetAccountAsync(int accountId) public async Task<AccountSummaryDto> GetAccountAsync(int accountId)
@@ -34,7 +78,11 @@ public class AccountService : IAccountService
.FirstOrDefaultAsync(a => a.Id == accountId) .FirstOrDefaultAsync(a => a.Id == accountId)
?? throw new InvalidOperationException($"Account {accountId} not found."); ?? throw new InvalidOperationException($"Account {accountId} not found.");
return MapToSummary(account); var totalIncome = account.Entries.Where(e => !e.IsDeleted && e.Type == EntryType.Income).Sum(e => e.Amount);
var totalExpense = account.Entries.Where(e => !e.IsDeleted && e.Type == EntryType.Expense).Sum(e => e.Amount);
var totalBalance = account.CarryoverBalance + totalIncome - totalExpense;
return new AccountSummaryDto(account.Id, account.Name, account.CarryoverBalance, totalBalance, account.CreatedUtc);
} }
public async Task<AccountSummaryDto> CreateAccountAsync(string name) public async Task<AccountSummaryDto> CreateAccountAsync(string name)
@@ -67,6 +115,27 @@ public class AccountService : IAccountService
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }
public async Task UpdateCarryoverAsync(int accountId, decimal carryoverBalance, int? year)
{
if (!year.HasValue)
{
await UpdateCarryoverAsync(accountId, carryoverBalance);
return;
}
await using var db = await _dbFactory.CreateDbContextAsync();
var account = await db.Accounts.FindAsync(accountId)
?? throw new InvalidOperationException($"Account {accountId} not found.");
var movementBeforeYear = await db.Entries
.Where(e => e.AccountId == accountId && !e.IsDeleted && e.Date.Year < year.Value)
.SumAsync(e => e.Type == EntryType.Income ? e.Amount : -e.Amount);
account.CarryoverBalance = carryoverBalance - movementBeforeYear;
await db.SaveChangesAsync();
}
public async Task DeleteAccountAsync(int accountId) public async Task DeleteAccountAsync(int accountId)
{ {
await using var db = await _dbFactory.CreateDbContextAsync(); await using var db = await _dbFactory.CreateDbContextAsync();
@@ -94,12 +163,4 @@ public class AccountService : IAccountService
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }
private static AccountSummaryDto MapToSummary(Account account)
{
var totalIncome = account.Entries.Where(e => e.Type == EntryType.Income).Sum(e => e.Amount);
var totalExpense = account.Entries.Where(e => e.Type == EntryType.Expense).Sum(e => e.Amount);
var totalBalance = account.CarryoverBalance + totalIncome - totalExpense;
return new AccountSummaryDto(account.Id, account.Name, account.CarryoverBalance, totalBalance, account.CreatedUtc);
}
} }

View File

@@ -12,7 +12,7 @@ public class BalanceQueryService : IBalanceQueryService
public BalanceQueryService(IDbContextFactory<FinanceDbContext> dbFactory) => _dbFactory = dbFactory; public BalanceQueryService(IDbContextFactory<FinanceDbContext> dbFactory) => _dbFactory = dbFactory;
public async Task<AccountBalanceDto> GetAccountBalanceAsync(int accountId) public async Task<AccountBalanceDto> GetAccountBalanceAsync(int accountId, int? year = null)
{ {
await using var db = await _dbFactory.CreateDbContextAsync(); await using var db = await _dbFactory.CreateDbContextAsync();
@@ -21,21 +21,33 @@ public class BalanceQueryService : IBalanceQueryService
.FirstOrDefaultAsync(a => a.Id == accountId) .FirstOrDefaultAsync(a => a.Id == accountId)
?? throw new InvalidOperationException($"Konto {accountId} nicht gefunden."); ?? throw new InvalidOperationException($"Konto {accountId} nicht gefunden.");
var currentYear = DateTime.Now.Year; var selectedYear = year ?? DateTime.Now.Year;
var activeEntries = account.Entries.Where(e => !e.IsDeleted).ToList(); var activeEntries = account.Entries.Where(e => !e.IsDeleted).ToList();
var totalIncome = activeEntries.Where(e => e.Type == EntryType.Income).Sum(e => e.Amount); var totalIncome = activeEntries.Where(e => e.Type == EntryType.Income).Sum(e => e.Amount);
var totalExpense = activeEntries.Where(e => e.Type == EntryType.Expense).Sum(e => e.Amount); var totalExpense = activeEntries.Where(e => e.Type == EntryType.Expense).Sum(e => e.Amount);
var currentYearIncome = activeEntries.Where(e => e.Type == EntryType.Income && e.Date.Year == currentYear).Sum(e => e.Amount);
var currentYearExpense = activeEntries.Where(e => e.Type == EntryType.Expense && e.Date.Year == currentYear).Sum(e => e.Amount); var selectedYearIncome = activeEntries
var totalBalance = account.CarryoverBalance + totalIncome - totalExpense; .Where(e => e.Type == EntryType.Income && e.Date.Year == selectedYear)
.Sum(e => e.Amount);
var selectedYearExpense = activeEntries
.Where(e => e.Type == EntryType.Expense && e.Date.Year == selectedYear)
.Sum(e => e.Amount);
var selectedYearCarryover = account.CarryoverBalance + activeEntries
.Where(e => e.Date.Year < selectedYear)
.Sum(e => e.Type == EntryType.Income ? e.Amount : -e.Amount);
var totalBalance = year.HasValue
? selectedYearCarryover + selectedYearIncome - selectedYearExpense
: account.CarryoverBalance + totalIncome - totalExpense;
return new AccountBalanceDto( return new AccountBalanceDto(
account.CarryoverBalance, year.HasValue ? selectedYearCarryover : account.CarryoverBalance,
totalIncome, totalIncome,
totalExpense, totalExpense,
currentYearIncome, selectedYearIncome,
currentYearExpense, selectedYearExpense,
totalBalance); totalBalance);
} }
} }

View File

@@ -13,14 +13,20 @@ public class EntryService : IEntryService
public EntryService(IDbContextFactory<FinanceDbContext> dbFactory) => _dbFactory = dbFactory; public EntryService(IDbContextFactory<FinanceDbContext> dbFactory) => _dbFactory = dbFactory;
public async Task<List<EntryDto>> GetEntriesAsync(int accountId, bool currentYearOnly) public Task<List<EntryDto>> GetEntriesAsync(int accountId, bool currentYearOnly)
{
var year = currentYearOnly ? DateTime.Now.Year : (int?)null;
return GetEntriesAsync(accountId, year);
}
public async Task<List<EntryDto>> GetEntriesAsync(int accountId, int? year)
{ {
await using var db = await _dbFactory.CreateDbContextAsync(); await using var db = await _dbFactory.CreateDbContextAsync();
var query = db.Entries.Where(e => e.AccountId == accountId); var query = db.Entries.Where(e => e.AccountId == accountId);
if (currentYearOnly) if (year.HasValue)
query = query.Where(e => e.Date.Year == DateTime.Now.Year); query = query.Where(e => e.Date.Year == year.Value);
var entries = await query var entries = await query
.OrderBy(e => e.Date) .OrderBy(e => e.Date)
@@ -59,6 +65,29 @@ public class EntryService : IEntryService
}).ToList(); }).ToList();
} }
public async Task<List<int>> GetEntryYearsAsync(int accountId)
{
await using var db = await _dbFactory.CreateDbContextAsync();
return await db.Entries
.Where(e => e.AccountId == accountId)
.Select(e => e.Date.Year)
.Distinct()
.OrderByDescending(y => y)
.ToListAsync();
}
public async Task<List<int>> GetAllEntryYearsAsync()
{
await using var db = await _dbFactory.CreateDbContextAsync();
return await db.Entries
.Select(e => e.Date.Year)
.Distinct()
.OrderByDescending(y => y)
.ToListAsync();
}
public async Task<EntryDto> CreateEntryAsync(int accountId, EntryType type, DateTime date, string title, decimal amount) public async Task<EntryDto> CreateEntryAsync(int accountId, EntryType type, DateTime date, string title, decimal amount)
{ {
await using var db = await _dbFactory.CreateDbContextAsync(); await using var db = await _dbFactory.CreateDbContextAsync();

View File

@@ -27,19 +27,25 @@ public class PdfStatementService : IPdfStatementService
_settingsService = settingsService; _settingsService = settingsService;
} }
public async Task<byte[]> GenerateStatementAsync(int accountId, bool currentYearOnly) public Task<byte[]> GenerateStatementAsync(int accountId, bool currentYearOnly)
{
var year = currentYearOnly ? DateTime.Now.Year : (int?)null;
return GenerateStatementAsync(accountId, year);
}
public async Task<byte[]> GenerateStatementAsync(int accountId, int? year)
{ {
await using var db = await _dbFactory.CreateDbContextAsync(); await using var db = await _dbFactory.CreateDbContextAsync();
var account = await db.Accounts.FindAsync(accountId) var account = await db.Accounts.FindAsync(accountId)
?? throw new InvalidOperationException($"Konto {accountId} nicht gefunden."); ?? throw new InvalidOperationException($"Konto {accountId} nicht gefunden.");
var entries = await _entryService.GetEntriesAsync(accountId, currentYearOnly); var entries = await _entryService.GetEntriesAsync(accountId, year);
var balance = await _balanceQueryService.GetAccountBalanceAsync(accountId); var balance = await _balanceQueryService.GetAccountBalanceAsync(accountId, year);
var clubName = await _settingsService.GetClubNameAsync() ?? "Mein Verein"; var clubName = await _settingsService.GetClubNameAsync() ?? "Mein Verein";
var title = currentYearOnly var title = year.HasValue
? $"{account.Name} Auszug {DateTime.Now.Year}" ? $"{account.Name} Auszug {year.Value}"
: $"{account.Name} Gesamtauszug"; : $"{account.Name} Gesamtauszug";
var document = Document.Create(container => var document = Document.Create(container =>
@@ -61,12 +67,6 @@ public class PdfStatementService : IPdfStatementService
page.Content().PaddingTop(15).Column(col => page.Content().PaddingTop(15).Column(col =>
{ {
col.Item().PaddingBottom(10).Row(row =>
{
row.RelativeItem().Text("Übertrag:").SemiBold();
row.AutoItem().Text(FormatCurrency(balance.CarryoverBalance)).SemiBold();
});
col.Item().Table(table => col.Item().Table(table =>
{ {
table.ColumnsDefinition(columns => table.ColumnsDefinition(columns =>
@@ -114,9 +114,16 @@ public class PdfStatementService : IPdfStatementService
col.Item().PaddingTop(20).LineHorizontal(1).LineColor(Colors.Grey.Lighten2); col.Item().PaddingTop(20).LineHorizontal(1).LineColor(Colors.Grey.Lighten2);
col.Item().PaddingTop(10).Column(summaryCol => col.Item().PaddingTop(10).Column(summaryCol =>
{ {
SummaryRow(summaryCol, "Übertrag:", balance.CarryoverBalance); var incomeValue = year.HasValue ? balance.CurrentYearIncome : balance.TotalIncome;
SummaryRow(summaryCol, "Einnahmen gesamt:", balance.TotalIncome); var expenseValue = year.HasValue ? balance.CurrentYearExpense : balance.TotalExpense;
SummaryRow(summaryCol, "Ausgaben gesamt:", -balance.TotalExpense);
if (year.HasValue)
{
SummaryRow(summaryCol, $"Übertrag von {year.Value - 1}:", balance.CarryoverBalance);
}
SummaryRow(summaryCol, year.HasValue ? "Einnahmen:" : "Einnahmen gesamt:", incomeValue);
SummaryRow(summaryCol, year.HasValue ? "Ausgaben:" : "Ausgaben gesamt:", -expenseValue);
summaryCol.Item().PaddingTop(5).Row(row => summaryCol.Item().PaddingTop(5).Row(row =>
{ {
row.RelativeItem().Text("Saldo:").Bold().FontSize(12); row.RelativeItem().Text("Saldo:").Bold().FontSize(12);
@@ -139,7 +146,12 @@ public class PdfStatementService : IPdfStatementService
return document.GeneratePdf(); return document.GeneratePdf();
} }
public async Task<byte[]> GenerateDashboardStatementAsync() public Task<byte[]> GenerateDashboardStatementAsync()
{
return GenerateDashboardStatementAsync(null);
}
public async Task<byte[]> GenerateDashboardStatementAsync(int? year)
{ {
await using var db = await _dbFactory.CreateDbContextAsync(); await using var db = await _dbFactory.CreateDbContextAsync();
@@ -162,20 +174,65 @@ public class PdfStatementService : IPdfStatementService
transferByEntryId[transfer.TargetEntryId] = transfer; transferByEntryId[transfer.TargetEntryId] = transfer;
} }
var entries = await db.Entries var entriesQuery = db.Entries
.Include(e => e.Account) .Include(e => e.Account)
.Where(e => !e.IsDeleted) .Where(e => !e.IsDeleted);
if (year.HasValue)
{
entriesQuery = entriesQuery.Where(e => e.Date.Year == year.Value);
}
var entries = await entriesQuery
.OrderBy(e => e.Date) .OrderBy(e => e.Date)
.ThenBy(e => e.CreatedUtc) .ThenBy(e => e.CreatedUtc)
.ToListAsync(); .ToListAsync();
var movementByAccountId = entries Dictionary<int, decimal> balanceByAccountId;
.GroupBy(e => e.AccountId) Dictionary<int, decimal>? carryoverByAccountId = null;
.ToDictionary(
g => g.Key,
g => g.Sum(e => e.Type == EntryType.Income ? e.Amount : -e.Amount));
var totalClubBalance = accounts.Sum(a => a.CarryoverBalance + movementByAccountId.GetValueOrDefault(a.Id)); if (year.HasValue)
{
var movementBeforeYearByAccountId = await db.Entries
.Where(e => !e.IsDeleted && e.Date.Year < year.Value)
.GroupBy(e => e.AccountId)
.ToDictionaryAsync(
g => g.Key,
g => g.Sum(e => e.Type == EntryType.Income ? e.Amount : -e.Amount));
var movementInYearByAccountId = entries
.GroupBy(e => e.AccountId)
.ToDictionary(
g => g.Key,
g => g.Sum(e => e.Type == EntryType.Income ? e.Amount : -e.Amount));
carryoverByAccountId = accounts
.ToDictionary(
a => a.Id,
a => a.CarryoverBalance + movementBeforeYearByAccountId.GetValueOrDefault(a.Id));
balanceByAccountId = accounts
.ToDictionary(
a => a.Id,
a => a.CarryoverBalance
+ movementBeforeYearByAccountId.GetValueOrDefault(a.Id)
+ movementInYearByAccountId.GetValueOrDefault(a.Id));
}
else
{
var movementByAccountId = entries
.GroupBy(e => e.AccountId)
.ToDictionary(
g => g.Key,
g => g.Sum(e => e.Type == EntryType.Income ? e.Amount : -e.Amount));
balanceByAccountId = accounts
.ToDictionary(
a => a.Id,
a => a.CarryoverBalance + movementByAccountId.GetValueOrDefault(a.Id));
}
var totalClubBalance = balanceByAccountId.Values.Sum();
var processedTransferIds = new HashSet<int>(); var processedTransferIds = new HashSet<int>();
var rows = new List<DashboardStatementRow>(); var rows = new List<DashboardStatementRow>();
@@ -236,7 +293,10 @@ public class PdfStatementService : IPdfStatementService
page.Header().Column(col => page.Header().Column(col =>
{ {
col.Item().Text(clubName).Bold().FontSize(18); col.Item().Text(clubName).Bold().FontSize(18);
col.Item().Text("Übersicht aller Konten").FontSize(13).FontColor(Colors.Grey.Darken1); var headerTitle = year.HasValue
? $"Übersicht aller Konten ({year.Value})"
: "Übersicht aller Konten";
col.Item().Text(headerTitle).FontSize(13).FontColor(Colors.Grey.Darken1);
col.Item().PaddingTop(2).Text($"Gesamtvermögen: {FormatCurrency(totalClubBalance)}") col.Item().PaddingTop(2).Text($"Gesamtvermögen: {FormatCurrency(totalClubBalance)}")
.FontSize(10) .FontSize(10)
.SemiBold() .SemiBold()
@@ -273,6 +333,18 @@ public class PdfStatementService : IPdfStatementService
} }
}); });
if (year.HasValue && carryoverByAccountId is not null)
{
BodyCell(table, string.Empty);
BodyCell(table, string.Empty);
BodyCell(table, $"Übertrag von {year.Value - 1}", alignRight: true);
foreach (var account in accounts)
{
BodyAmountCell(table, carryoverByAccountId.GetValueOrDefault(account.Id));
}
}
foreach (var row in rows) foreach (var row in rows)
{ {
BodyCell(table, row.DisplayId); BodyCell(table, row.DisplayId);
@@ -291,6 +363,16 @@ public class PdfStatementService : IPdfStatementService
} }
} }
} }
// Add one summary line with the final balance for each account column.
FooterCell(table, string.Empty);
FooterCell(table, string.Empty);
FooterCell(table, "Kontostand", alignRight: true);
foreach (var account in accounts)
{
FooterAmountCell(table, balanceByAccountId.GetValueOrDefault(account.Id));
}
}); });
content.Item().PaddingTop(8).AlignRight().Text(text => content.Item().PaddingTop(8).AlignRight().Text(text =>
@@ -352,6 +434,31 @@ public class PdfStatementService : IPdfStatementService
.FontColor(amount >= 0 ? Colors.Green.Darken1 : Colors.Red.Darken1); .FontColor(amount >= 0 ? Colors.Green.Darken1 : Colors.Red.Darken1);
} }
private static void FooterCell(TableDescriptor table, string text, bool alignRight = false)
{
var cell = table.Cell()
.BorderTop(1)
.BorderColor(Colors.Grey.Darken1)
.PaddingTop(5)
.PaddingBottom(2);
var content = alignRight ? cell.AlignRight() : cell;
content.Text(text).SemiBold();
}
private static void FooterAmountCell(TableDescriptor table, decimal amount)
{
table.Cell()
.BorderTop(1)
.BorderColor(Colors.Grey.Darken1)
.PaddingTop(5)
.PaddingBottom(2)
.AlignRight()
.Text(FormatCurrency(amount))
.SemiBold()
.FontColor(amount >= 0 ? Colors.Green.Darken1 : Colors.Red.Darken1);
}
private static int ParseDisplayIdYear(string displayId) private static int ParseDisplayIdYear(string displayId)
{ {
var parts = displayId.Split('-'); var parts = displayId.Split('-');