Show bankpayment matching

This commit is contained in:
Claudio Schaad 2025-05-08 19:35:13 +02:00
parent 0828dd7691
commit 0c537f8816
7 changed files with 148 additions and 40 deletions

View file

@ -1,15 +0,0 @@
using System.Globalization;
using System.Text;
namespace Schaad.Accounting.Common.Extensions
{
public static class DecimalExtension
{
public static string ToFormattedString(this decimal value)
{
var culture = new CultureInfo("de-CH");
culture.NumberFormat.NumberGroupSeparator = "'";
return value.ToString("#,0.00", culture);
}
}
}

View file

@ -14,4 +14,7 @@
<HintPath>..\dlls\Schaad.Finance.Api.dll</HintPath> <HintPath>..\dlls\Schaad.Finance.Api.dll</HintPath>
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Interfaces\Extensions\" />
</ItemGroup>
</Project> </Project>

View file

@ -1,8 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Schaad.Accounting.Common.Extensions;
using Schaad.Accounting.Datasets; using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
using Schaad.Finance.Api; using Schaad.Finance.Api;
@ -133,18 +133,25 @@ namespace Schaad.Accounting.Services
if (trx.OriginAccountId == accountId) if (trx.OriginAccountId == accountId)
{ {
debit = trx.Value.ToFormattedString(); debit = ToFormattedString(trx.Value);
trx.Value *= -1; trx.Value *= -1;
} }
else else
{ {
credit = trx.Value.ToFormattedString(); credit = ToFormattedString(trx.Value);
} }
balance += trx.Value; balance += trx.Value;
sb.AppendLine($"{trx.BookingDate:dd.MM.yyyy};{trx.ValueDate:dd.MM.yyyy};{trx.Text};{debit};{credit};{balance.ToFormattedString()}"); sb.AppendLine($"{trx.BookingDate:dd.MM.yyyy};{trx.ValueDate:dd.MM.yyyy};{trx.Text};{debit};{credit};{ToFormattedString(balance)}");
} }
var fileBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(sb.ToString()); var fileBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(sb.ToString());
return fileBytes; return fileBytes;
} }
private string ToFormattedString(decimal value)
{
var culture = new CultureInfo("de-CH");
culture.NumberFormat.NumberGroupSeparator = "'";
return value.ToString("#,0.00", culture);
}
} }
} }

View file

@ -1,4 +1,5 @@
@page "/" @page "/"
@rendermode InteractiveServer @rendermode InteractiveServer
<PageTitle>Home</PageTitle> <PageTitle>Home</PageTitle>
@ -26,19 +27,53 @@
Cancel Cancel
</FluentButton> </FluentButton>
<div id="datagrid-container"> <table>
<FluentDataGrid Items="@bankTransactionsQueryable" RowSize="DataGridRowSize.Small" AutoItemsPerPage="true"> <thead>
<ChildContent> <tr>
<PropertyColumn Title="Buchungsdatum" Width="170px" Property="@(p => p.BookingDate)" Format="dd.MM.yyyy" Sortable="true" IsDefaultSortColumn="true" InitialSortDirection="SortDirection.Descending"/> <th>Buchungsdatum</th>
<PropertyColumn Title="Valutadatum" Width="170px" Property="@(p => p.ValueDate)" Format="dd.MM.yyyy" Sortable="true"/> <th>Valutadatum</th>
<PropertyColumn Title="Text" Property="@(p => p.Text)"/> <th>Text</th>
<PropertyColumn Title="Betrag" Width="150px" Property="@(p => p.Value)" Format="C2" Align="Align.End"/> <th>Betrag</th>
</ChildContent> <th>Konto</th>
<EmptyContent> <th>Zusatzinfo</th>
<FluentStack Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center" HorizontalAlignment="HorizontalAlignment.Center" HorizontalGap="10"> <th></th>
<FluentIcon Value="@(new Icons.Filled.Size24.Crown())" Color="@Color.Accent"/> <th></th>
<div>Keine Daten vorhanden!</div> </tr>
</FluentStack> </thead>
</EmptyContent> <tbody>
</FluentDataGrid> @foreach (var transaction in matchingBankTransactions)
</div> {
<tr>
<td><div>@transaction.BookingDate.ToString("dd.MM.yyyy")</div></td>
<td><div>@transaction.ValueDate.ToString("dd.MM.yyyy")</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 class="betrag-column">
<div>@transaction.Value.ToFormattedString()</div>
</td>
<td>
<select @bind="@transaction.TargetAccountId">
@foreach (var account in accounts.OrderBy(a=> a.Number).ThenBy(a => a.Name))
{
<option value="@account.Id">@($"{account.Number} / {@account.Name}")</option>
}
</select>
</td>
<td class="zusatzinfo-column" title="@transaction.BankTransactionText">
<div>@transaction.BankTransactionText</div>
</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>
</table>

View file

@ -1,4 +1,5 @@
using System.IO.Compression; using System.Diagnostics;
using System.IO.Compression;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components;
using Schaad.Accounting.Datasets; using Schaad.Accounting.Datasets;
@ -24,7 +25,15 @@ public partial class Home : ComponentBase
[Inject] [Inject]
private IFileService fileService { get; set; } = null!; private IFileService fileService { get; set; } = null!;
private IQueryable<BankTransaction>? bankTransactionsQueryable; [Inject]
private IBookingTextRepository bookingTextRepository { get; set; } = null!;
[Inject]
private ITransactionRepository transactionRepository { get; set; } = null!;
private List<Transaction>? matchingBankTransactions;
private IReadOnlyList<Account> accounts = [];
private IReadOnlyList<string> bookingTexts = [];
FluentInputFile? myFileByBuffer; FluentInputFile? myFileByBuffer;
int? progressPercent; int? progressPercent;
@ -32,9 +41,17 @@ public partial class Home : ComponentBase
bool IsCanceled; bool IsCanceled;
Dictionary<int, string> Files = new(); Dictionary<int, string> Files = new();
private bool loaded;
protected override Task OnInitializedAsync() protected override Task OnInitializedAsync()
{ {
bankTransactionsQueryable = viewService.GetOpenBankTransactionList().AsQueryable(); if (loaded == false) {
matchingBankTransactions = viewService.MatchOpenBankTransactions();
accounts = viewService.GetAccountViewList();
bookingTexts = bookingTextRepository.GetBookingTextList().Select(b => b.Text).ToArray();
loaded = true;
}
return base.OnInitializedAsync(); return base.OnInitializedAsync();
} }
@ -82,7 +99,7 @@ public partial class Home : ComponentBase
File.Delete(file.Value); File.Delete(file.Value);
} }
bankTransactionsQueryable = viewService.GetOpenBankTransactionList().AsQueryable(); matchingBankTransactions = viewService.MatchOpenBankTransactions();
} }
private async Task ImportXmlAndShowResultAsync(string fileName) private async Task ImportXmlAndShowResultAsync(string fileName)
@ -134,4 +151,22 @@ public partial class Home : ComponentBase
{ {
toastService.ShowToast(intent, message, 3000); toastService.ShowToast(intent, message, 3000);
} }
private async Task BookBankTransactionAsync(Transaction transaction)
{
if (string.IsNullOrEmpty(transaction.Text) || string.IsNullOrEmpty(transaction.TargetAccountId))
{
ShowToast("Transaktion kann wegen fehlenden Daten nicht gespeichert werden!", ToastIntent.Error);
return;
}
transactionRepository.SaveTransaction(transaction);
matchingBankTransactions!.RemoveAll(q => q == transaction);
await Task.CompletedTask;
}
private async Task SplitBankTransactionAsync(Transaction transaction)
{
ShowToast("Split noch nicht implementiert!", ToastIntent.Warning);
await Task.CompletedTask;
}
} }

View file

@ -0,0 +1,42 @@
table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
padding-right: 10px;
}
th, td {
padding: 4px 4px;
vertical-align: middle;
overflow: hidden;
border: 1px solid #ccc;
}
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(7), td:nth-child(7),
th:nth-child(8), td:nth-child(8) { width: 40px; text-align: center; } /* Buttons */
.text-column input {
width: 100%;
box-sizing: border-box; /* Ensures padding/border doesn't overflow */
}
.zusatzinfo-column div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.betrag-column {
text-align: right;
white-space: nowrap;
}

View file

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