feat(accounts): preview and confirm cross-year balance corrections (U12)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<ModalDialog @ref="modal" Title="Neues Konto" OnCancel="OnCancel" Dirty="@(!string.IsNullOrWhiteSpace(name))">
|
||||
@inject ToastService Toasts
|
||||
<ModalDialog @ref="modal" Title="Neues Konto" OnCancel="OnCancel" Dirty="@(!string.IsNullOrWhiteSpace(name) || initialAmount != "0,00")" Busy="saving">
|
||||
<h5 class="mb-3">Neues Konto</h5>
|
||||
<form @onsubmit="Save" @onsubmit:preventDefault="true">
|
||||
<div class="form-container">
|
||||
@@ -6,8 +7,11 @@
|
||||
<input id="account-name" data-initial-focus type="text" class="form-control" @bind="name" @bind:event="oninput"
|
||||
placeholder="z.B. Girokonto" />
|
||||
</div>
|
||||
<label for="account-initial">Anfangsbestand (€)</label>
|
||||
<AmountInput Id="account-initial" @bind-Value="initialAmount" AllowNegative="true" Disabled="saving" />
|
||||
<p>Geld, das vor der ersten erfassten Buchung vorhanden ist. 0 ist möglich, ein Minus bei überzogenem Konto ebenfalls. Der Anfangsbestand zählt nicht als neue Einnahme.</p>
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button type="submit" class="btn btn-success" disabled="@string.IsNullOrWhiteSpace(name)"><i class="bi bi-check-lg"></i> Erstellen</button>
|
||||
<button type="submit" class="btn btn-success" disabled="@(saving || string.IsNullOrWhiteSpace(name) || !ValidInitial)"><i class="bi bi-check-lg"></i> Erstellen</button>
|
||||
<button type="button" class="btn btn-outline-secondary" @onclick="Cancel"><i class="bi bi-x-lg"></i> Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -7,7 +7,7 @@ public partial class AddAccountDialog
|
||||
#region Parameters
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> OnSave { get; set; }
|
||||
public EventCallback<Duempelkas.App.Services.Models.AccountSetup> OnSave { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback OnCancel { get; set; }
|
||||
@@ -17,6 +17,9 @@ public partial class AddAccountDialog
|
||||
#region Fields
|
||||
|
||||
private string name = string.Empty;
|
||||
private string initialAmount = "0,00";
|
||||
private bool ValidInitial => Duempelkas.App.Services.BookingInput.TryBalance(initialAmount, out _);
|
||||
private bool saving;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -24,9 +27,12 @@ public partial class AddAccountDialog
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
if (!saving && !string.IsNullOrWhiteSpace(name) && ValidInitial)
|
||||
{
|
||||
await OnSave.InvokeAsync(name.Trim());
|
||||
saving = true;
|
||||
try { Duempelkas.App.Services.BookingInput.TryBalance(initialAmount, out var initial); await OnSave.InvokeAsync(new(name.Trim(), initial)); }
|
||||
catch (Exception ex) { Toasts.Show("Das Konto konnte nicht angelegt werden. Die Eingaben bleiben erhalten.", Duempelkas.App.Services.ToastKind.Error, ex.Message); }
|
||||
finally { saving = false; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
<ModalDialog @ref="modal" Title="Bestand korrigieren" OnCancel="OnCancel" Dirty="@(amount != CurrentAmount)">
|
||||
<h5 class="mb-3">Übertrag bearbeiten</h5>
|
||||
<div class="form-container">
|
||||
<label class="form-label" for="carryover">Übertrag (€)</label>
|
||||
<input id="carryover" data-initial-focus type="number" class="form-control no-spinner" step="0.01" @bind="amount" />
|
||||
</div>
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button type="button" class="btn btn-success" @onclick="Save"><i class="bi bi-check-lg"></i> Speichern</button>
|
||||
<button type="button" class="btn btn-outline-secondary" @onclick="Cancel"><i class="bi bi-x-lg"></i> Abbrechen</button>
|
||||
</div>
|
||||
@inject ToastService Toasts
|
||||
<ModalDialog @ref="modal" Title="Bestandskorrektur" OnCancel="OnCancel" Dirty="@(amount != CurrentAmount)" Busy="saving">
|
||||
<h5>Bestandskorrektur – @AccountName</h5>
|
||||
<p>@(Year.HasValue ? $"Übertrag zum 01.01.{Year}" : "Gemeinsamer Anfangsbestand – alle Jahre")</p>
|
||||
<p>Anfangsbestand + Einnahmen − Ausgaben = Kontostand</p>
|
||||
<p>Dies ist eine spätere Korrektur, keine zusätzliche Einnahme oder Ausgabe.</p>
|
||||
<label for="carryover">@(Year.HasValue ? "Neuer Übertrag (€)" : "Neuer Anfangsbestand (€)")</label>
|
||||
<AmountInput Id="carryover" Value="amountText" ValueChanged="ChangeAmount" AllowNegative="true" Disabled="saving" />
|
||||
<div class="correction-preview">
|
||||
<p>Gewählter Bestand: @Money(CurrentAmount) → @Money(amount)</p>
|
||||
<p>Gemeinsamer Anfangsbestand: @Money(InitialBalance) → @Money(InitialBalance + Delta)</p>
|
||||
<p>Kontostand der Ansicht: @Money(CurrentEndBalance) → @Money(CurrentEndBalance + Delta)</p>
|
||||
<p><strong>Alle Kontostände dieses Kontos ändern sich um @Money(Delta). Das betrifft frühere und spätere Jahre.</strong></p>
|
||||
</div>
|
||||
<label><input type="checkbox" @bind="acknowledged" disabled="@saving" /> Ich habe die Auswirkungen auf alle Jahre geprüft.</label>
|
||||
<div class="dialog-actions">
|
||||
<button type="button" class="btn btn-success" disabled="@(saving || !ValidAmount || !acknowledged || Delta == 0m)" @onclick="Save">Korrektur speichern</button>
|
||||
<button type="button" class="btn btn-outline-secondary" disabled="@saving" data-safe-focus @onclick="Cancel">Abbrechen</button>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
|
||||
@code { private ModalDialog? modal; }
|
||||
|
||||
@@ -1,39 +1,36 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
using Duempelkas.App.Services;
|
||||
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() { if (modal != null) await modal.RequestCloseAsync(); else await OnCancel.InvokeAsync(); }
|
||||
|
||||
#endregion
|
||||
[Parameter] public decimal CurrentAmount { get; set; }
|
||||
[Parameter] public decimal InitialBalance { get; set; }
|
||||
[Parameter] public decimal CurrentEndBalance { get; set; }
|
||||
[Parameter] public int? Year { get; set; }
|
||||
[Parameter] public string AccountName { get; set; } = "Konto";
|
||||
[Parameter] public EventCallback<decimal> OnSave { get; set; }
|
||||
[Parameter] public EventCallback OnCancel { get; set; }
|
||||
private string amountText = string.Empty;
|
||||
private bool acknowledged;
|
||||
private bool initialized;
|
||||
private bool saving;
|
||||
private bool ValidAmount => BookingInput.TryBalance(amountText, out _);
|
||||
private decimal amount => BookingInput.TryBalance(amountText, out var value) ? value : CurrentAmount;
|
||||
private decimal Delta => amount - CurrentAmount;
|
||||
private static string Money(decimal value) => value.ToString("N2", System.Globalization.CultureInfo.GetCultureInfo("de-DE")) + " €";
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (!initialized) { amountText = CurrentAmount.ToString("0.00", System.Globalization.CultureInfo.GetCultureInfo("de-DE")); initialized = true; }
|
||||
}
|
||||
private void ChangeAmount(string value) { amountText = value; acknowledged = false; }
|
||||
private async Task Save()
|
||||
{
|
||||
if (saving || !ValidAmount || !acknowledged || Delta == 0m) return;
|
||||
saving = true;
|
||||
try { await OnSave.InvokeAsync(amount); }
|
||||
catch (Exception ex) { Toasts.Show("Die Korrektur konnte nicht gespeichert werden. Bitte den aktuellen Bestand prüfen.", ToastKind.Error, ex.Message); }
|
||||
finally { saving = false; }
|
||||
}
|
||||
private Task Cancel() => modal?.RequestCloseAsync() ?? Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
|
||||
@if (showEditCarryover)
|
||||
{
|
||||
<EditCarryoverDialog CurrentAmount="@(balance?.CarryoverBalance ?? 0)"
|
||||
<EditCarryoverDialog AccountName="@(account?.Name ?? "")" Year="selectedYear" InitialBalance="@(account?.CarryoverBalance ?? 0)" CurrentEndBalance="@(balance?.TotalBalance ?? 0)" CurrentAmount="@(balance?.CarryoverBalance ?? 0)"
|
||||
OnSave="HandleSaveCarryover"
|
||||
OnCancel="() => showEditCarryover = false" />
|
||||
}
|
||||
|
||||
@@ -110,9 +110,10 @@ public partial class AccountDetail
|
||||
|
||||
private async Task HandleSaveCarryover(decimal newAmount)
|
||||
{
|
||||
await AccountService.UpdateCarryoverAsync(AccountId, newAmount, selectedYear);
|
||||
showEditCarryover = false;
|
||||
await AccountService.CorrectCarryoverAsync(AccountId, newAmount, selectedYear, balance!.CarryoverBalance, account!.CarryoverBalance);
|
||||
await LoadAll();
|
||||
showEditCarryover = false;
|
||||
Toasts.Show("Bestandskorrektur gespeichert. Die Änderung gilt für alle Jahre.", ToastKind.Success);
|
||||
}
|
||||
|
||||
private Task HandleEntryCreated(BookingSaved saved) => HandleBookingSaved(saved);
|
||||
|
||||
@@ -84,9 +84,9 @@ public partial class Dashboard
|
||||
|
||||
#region Actions
|
||||
|
||||
private async Task HandleAccountCreated(string name)
|
||||
private async Task HandleAccountCreated(AccountSetup setup)
|
||||
{
|
||||
await AccountService.CreateAccountAsync(name);
|
||||
await AccountService.CreateAccountAsync(setup.Name, setup.InitialBalance);
|
||||
showAddAccount = false;
|
||||
await LoadAll();
|
||||
}
|
||||
|
||||
@@ -20,6 +20,14 @@ public static partial class BookingInput
|
||||
&& amount > 0;
|
||||
}
|
||||
|
||||
public static bool TryBalance(string text, out decimal amount)
|
||||
{
|
||||
amount = 0;
|
||||
var plain = text.Trim();
|
||||
return Regex.IsMatch(plain, @"^-?\d+([,.]\d{1,2})?$", RegexOptions.CultureInvariant)
|
||||
&& decimal.TryParse(plain.Replace(',', '.'), NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out amount);
|
||||
}
|
||||
|
||||
public static DateTime Date(string text) => DateTime.TryParseExact(text, "yyyy-MM-dd",
|
||||
CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : default;
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@ public interface IAccountService
|
||||
Task<List<AccountSummaryDto>> GetAllAccountsAsync(int? year);
|
||||
Task<AccountSummaryDto> GetAccountAsync(int accountId);
|
||||
Task<AccountSummaryDto> CreateAccountAsync(string name);
|
||||
Task<AccountSummaryDto> CreateAccountAsync(string name, decimal initialBalance);
|
||||
Task RenameAccountAsync(int accountId, string newName);
|
||||
Task UpdateCarryoverAsync(int accountId, decimal carryoverBalance);
|
||||
Task UpdateCarryoverAsync(int accountId, decimal carryoverBalance, int? year);
|
||||
Task CorrectCarryoverAsync(int accountId, decimal value, int? year, decimal expectedCarryover, decimal expectedInitial);
|
||||
Task DeleteAccountAsync(int accountId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Duempelkas.App.Services.Models;
|
||||
|
||||
public sealed record AccountSetup(string Name, decimal InitialBalance);
|
||||
Reference in New Issue
Block a user