Add transaction split dialog

This commit is contained in:
Claudio Schaad 2025-05-13 20:27:09 +02:00
parent e76644a122
commit c86edfdde2
14 changed files with 276 additions and 33 deletions

View file

@ -3,7 +3,7 @@ using Schaad.Accounting.Models;
namespace Schaad.Accounting.Datasets
{
public record TransactionDataset : Transaction
public class TransactionDataset : Transaction
{
public Account OriginAccount { get; set; }

View file

@ -5,7 +5,7 @@ using System.Linq;
namespace Schaad.Accounting.Models
{
public record Transaction
public class Transaction
{
public string Id { get; set; }
public string BankTransactionId { get; set; }

View file

@ -30,6 +30,8 @@ namespace Schaad.Accounting.Repositories
/// </summary>
public void SaveTransaction(Transaction transaction)
{
transaction.Value = Math.Abs(transaction.Value);
// prevent saving fxrate for chf account
var isFxAccount = accountRepository.GetAccount(transaction.OriginAccountId).IsFxAccount;

View file

@ -169,7 +169,7 @@ namespace Schaad.Accounting.Services
if (account.Class == ClassIds.Activa && accountId == t.OriginAccountId )
{
return t with { Value = -t.Value };
t.Value *= -1;
}
return t;

View file

@ -0,0 +1,7 @@
<select @bind="@AccountId" @oninput="OnInputChanged">
<option value=""></option>
@foreach (var account in Accounts.OrderBy(a => a.Number).ThenBy(a => a.Name))
{
<option value="@account.Id">@($"{account.Number} / {account.Name}")</option>
}
</select>

View file

@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Components;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.UI.Components.Pages.Controls;
public partial class AccountSelector : ComponentBase
{
[Parameter] public required string? AccountId { get; set; }
[Parameter] public required IReadOnlyList<Account> Accounts { get; set; }
[Parameter] public EventCallback<string> AccountIdChanged { get; set; }
private async Task OnInputChanged(ChangeEventArgs e)
{
AccountId = e.Value?.ToString();
await AccountIdChanged.InvokeAsync(AccountId);
}
}

View file

@ -0,0 +1,5 @@
select {
width: 100%;
height: 30px;
box-sizing: border-box;
}

View file

@ -0,0 +1,83 @@
@implements IDialogContentComponent<Transaction>
@rendermode InteractiveServer
<FluentDialogHeader ShowDismiss="true">
<FluentStack VerticalAlignment="VerticalAlignment.Center">
<FluentIcon Value="@(new Icons.Regular.Size24.WindowApps())"/>
<FluentLabel Typo="Typography.PaneHeader">
@Dialog.Instance.Parameters.Title
</FluentLabel>
</FluentStack>
</FluentDialogHeader>
<FluentDialogBody>
Total: @Math.Abs(Content.Value) / Rest: @openAmount
<FluentButton Appearance="Appearance.Neutral" IconEnd="@(new Icons.Regular.Size24.Add())"
OnClick="@AddTransactionAsync">
Transaktion hinzufügen
</FluentButton>
<EditForm EditContext="@editContext">
<DataAnnotationsValidator/>
<table>
<thead>
<tr>
<th>Datum</th>
<th>Betrag</th>
<th>Text</th>
<th>Konto</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var transaction in transactionList)
{
<tr>
<td>
<div>@transaction.ValueDate.ToString("dd.MM.yyyy")</div>
</td>
<td>
<div>
<InputNumber @bind-Value="@transaction.Value" @onblur="CalculateOpenAmount"/>
</div>
</td>
<td>
<div class="text-column">
<input list="texts" @bind="@transaction.Text" placeholder="Type or select..."/>
<datalist id="texts">
@foreach (var text in bookingTexts.OrderBy(t => t))
{
<option value="@text"></option>
}
</datalist>
</div>
</td>
<td>
<div class="account-column">
<AccountSelector Accounts="accounts" @bind-AccountId="@transaction.TargetAccountId" />
</div>
</td>
<td style="align-content: center">
<FluentButton IconEnd="@(new Icons.Regular.Size16.Delete())" Appearance="Appearance.Neutral"
OnClick="@(() => transactionList.Remove(transaction))"/>
</td>
</tr>
}
</tbody>
</table>
<div>
<FluentValidationSummary/>
</div>
</EditForm>
</FluentDialogBody>
<FluentDialogFooter>
<FluentButton Appearance="Appearance.Neutral"
OnClick="@CancelAsync">
Abbrechen
</FluentButton>
<FluentButton Appearance="Appearance.Accent" Disabled="@(openAmount != 0)"
OnClick="@SaveAsync">
Speichern
</FluentButton>
</FluentDialogFooter>

View file

@ -0,0 +1,87 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.FluentUI.AspNetCore.Components;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.UI.Components.Pages.Dialogs;
public partial class TransactionSplitDialog : ComponentBase
{
private EditContext editContext = null!;
[CascadingParameter]
public FluentDialog Dialog { get; set; } = null!;
[Parameter]
public Transaction Content { get; set; } = null!;
[Inject]
private ITransactionRepository transactionRepository { get; set; } = null!;
[Inject]
private IBankTransactionRepository bankTransactionRepository { get; set; } = null!;
[Inject]
private IAccountRepository accountRepository { get; set; } = null!;
[Inject]
private IBookingTextRepository bookingTextRepository { get; set; } = null!;
[Inject]
private IViewService viewService { get; set; } = null!;
private List<Transaction> transactionList = new();
private IReadOnlyList<AccountDataset> accounts = null!;
private IReadOnlyList<string> bookingTexts = [];
private decimal openAmount = 0;
protected override void OnInitialized()
{
accounts = viewService.GetAccountViewList();
bookingTexts = bookingTextRepository.GetBookingTextList().Select(b => b.Text).ToArray();
var accountList = accountRepository.GetAccountList();
var bankTrx = bankTransactionRepository.GetBankTransaction(Content.BankTransactionId);
var trx = new Transaction(bankTrx, accountList);
trx.Value = Math.Abs(bankTrx.Value);
transactionList.Add(trx);
editContext = new EditContext(trx);
}
private async Task AddTransactionAsync()
{
transactionList.Add(new Transaction
{
ValueDate = Content.ValueDate,
BookingDate = Content.BookingDate,
OriginAccountId = Content.OriginAccountId,
Value = 0
});
await Task.CompletedTask;
}
private void CalculateOpenAmount()
{
openAmount = Content.Value + transactionList.Sum(t => t.Value);
}
private async Task SaveAsync()
{
if (editContext.Validate())
{
foreach (var transtaction in transactionList)
{
transactionRepository.SaveTransaction(transtaction);
}
await Dialog.CloseAsync(Content);
}
}
private async Task CancelAsync()
{
await Dialog.CancelAsync();
}
}

View file

@ -0,0 +1,10 @@
th:nth-child(1), td:nth-child(1) { width: 100px; }
th:nth-child(2), td:nth-child(2) { width: 50px; }
th:nth-child(3), td:nth-child(3) { width: 200px; }
th:nth-child(4), td:nth-child(4) { width: 200px; }
th:nth-child(5), td:nth-child(5) { width: 40px; text-align: center; }
.text-column input {
width: 100%;
box-sizing: border-box;
}

View file

@ -19,11 +19,13 @@
@progressTitle
</FluentLabel>
<FluentButton Appearance="Appearance.Lightweight" IconStart="@(new Icons.Regular.Size16.ArrowUpload())" Id="MyUploadBuffer">
<FluentButton Appearance="Appearance.Lightweight" IconStart="@(new Icons.Regular.Size16.ArrowUpload())"
Id="MyUploadBuffer">
Upload files
</FluentButton>
<FluentButton Appearance="Appearance.Lightweight" IconStart="@(new Icons.Regular.Size16.Dismiss())" @onclick="@((e) => IsCanceled = true)">
<FluentButton Appearance="Appearance.Lightweight" IconStart="@(new Icons.Regular.Size16.Dismiss())"
@onclick="@((e) => IsCanceled = true)">
Cancel
</FluentButton>
@ -38,10 +40,10 @@ else
<tr>
<th>Buchungsdatum</th>
<th>Valutadatum</th>
<th>Text</th>
<th>Betrag</th>
<th>Konto</th>
<th>Zusatzinfo</th>
<th>Text</th>
<th>Konto</th>
<th></th>
<th></th>
</tr>
@ -51,9 +53,17 @@ else
{
<tr>
<td>
<div>@transaction.BookingDate.ToString("dd.MM.yyyy")</div></td>
<div>@transaction.BookingDate.ToString("dd.MM.yyyy")</div>
</td>
<td>
<div>@transaction.ValueDate.ToString("dd.MM.yyyy")</div></td>
<div>@transaction.ValueDate.ToString("dd.MM.yyyy")</div>
</td>
<td class="betrag-column">
<div>@Math.Abs(transaction.Value).ToFormattedString()</div>
</td>
<td class="zusatzinfo-column" title="@transaction.BankTransactionText">
<div>@transaction.BankTransactionText</div>
</td>
<td>
<div class="text-column">
<input list="texts" @bind="@transaction.Text" placeholder="Type or select..."/>
@ -65,23 +75,24 @@ else
</datalist>
</div>
</td>
<td class="betrag-column">
<div>@transaction.Value.ToFormattedString()</div>
</td>
<td>
<select @bind="@transaction.TargetAccountId">
<option value=""></option>
@foreach (var account in accounts.OrderBy(a => a.Number).ThenBy(a => a.Name))
@if (transaction.Value > 0)
{
<option value="@account.Id">@($"{account.Number} / {@account.Name}")</option>
<AccountSelector Accounts="accounts" @bind-AccountId="@transaction.OriginAccountId" />
}
else
{
<AccountSelector Accounts="accounts" @bind-AccountId="@transaction.TargetAccountId" />
}
</select>
</td>
<td class="zusatzinfo-column" title="@transaction.BankTransactionText">
<div>@transaction.BankTransactionText</div>
<td style="align-content: center">
<FluentButton IconEnd="@(new Icons.Regular.Size16.SplitVertical())" Appearance="Appearance.Neutral"
OnClick="@(() => SplitBankTransactionAsync(transaction))" Disabled="@(transaction.Value > 0)"/>
</td>
<td style="align-content: center">
<FluentButton IconEnd="@(new Icons.Regular.Size16.Save())" Appearance="Appearance.Neutral"
OnClick="@(() => BookBankTransactionAsync(transaction))"/>
</td>
<td style="align-content: center"><FluentButton IconEnd="@(new Icons.Regular.Size16.SplitVertical())" Appearance="Appearance.Neutral" OnClick="@(() => SplitBankTransactionAsync(transaction))"/></td>
<td style="align-content: center"><FluentButton IconEnd="@(new Icons.Regular.Size16.Save())" Appearance="Appearance.Neutral" OnClick="@(() => BookBankTransactionAsync(transaction))"/></td>
</tr>
}
</tbody>

View file

@ -5,6 +5,7 @@ using Microsoft.FluentUI.AspNetCore.Components;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
using Schaad.Accounting.UI.Components.Pages.Dialogs;
namespace Schaad.Accounting.UI.Components.Pages;
@ -19,6 +20,9 @@ public partial class Home : ComponentBase
[Inject]
private IMessageService messageService { get; set; } = null!;
[Inject]
private IDialogService dialogService { get; set; } = null!;
[Inject]
private ISettingsService settingsService { get; set; } = null!;
@ -166,7 +170,20 @@ public partial class Home : ComponentBase
private async Task SplitBankTransactionAsync(Transaction transaction)
{
ShowToast("Split noch nicht implementiert!", ToastIntent.Warning);
var dialog = await dialogService.ShowDialogAsync<TransactionSplitDialog>(transaction, new DialogParameters()
{
Height = "500px",
Width = "800px",
Title = "Split Transaktion",
PreventDismissOnOverlayClick = true,
PreventScroll = true,
});
var result = await dialog.Result;
if (!result.Cancelled && result.Data != null)
{
matchingBankTransactions!.RemoveAll(q => q == transaction);
await Task.CompletedTask;
}
}
}

View file

@ -16,16 +16,17 @@ th {
background-color: #f8f8f8;
}
th:nth-child(1), td:nth-child(1) { width: 120px; } /* Buchungsdatum */
th:nth-child(2), td:nth-child(2) { width: 120px; } /* Valutadatum */
th:nth-child(3), td:nth-child(3) { width: 240px; } /* Text */
th:nth-child(4), td:nth-child(4) { width: 100px; text-align: right; } /* Betrag */
th:nth-child(5), td:nth-child(5) { width: 300px; } /* Konto */
th:nth-child(1), td:nth-child(1) { width: 120px; }
th:nth-child(2), td:nth-child(2) { width: 120px; }
th:nth-child(3), td:nth-child(3) { width: 100px; text-align: right; }
th:nth-child(5), td:nth-child(5) { width: 240px; }
th:nth-child(6), td:nth-child(6) { width: 300px; }
th:nth-child(7), td:nth-child(7),
th:nth-child(8), td:nth-child(8) { width: 40px; text-align: center; } /* Buttons */
th:nth-child(8), td:nth-child(8) { width: 40px; text-align: center; }
.text-column input {
width: 100%;
height: 30px;
box-sizing: border-box; /* Ensures padding/border doesn't overflow */
}

View file

@ -13,6 +13,7 @@
@using Plotly.Blazor
@using Schaad.Accounting.UI
@using Schaad.Accounting.UI.Components
@using Schaad.Accounting.UI.Components.Pages.Controls
@using Schaad.Accounting.Models
@using Schaad.Accounting.Interfaces