Add bank transaction import

This commit is contained in:
Claudio Schaad 2025-05-06 20:47:00 +02:00
parent e2802dd797
commit 0828dd7691
8 changed files with 188 additions and 43 deletions

View file

@ -1,5 +0,0 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABar_002Ecs_002Fl_003AC_0021_003FUsers_003Fschaa_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F3d754f61d7ca16fefcc624d522ddfb3235d54b85ab5638c1e181cde5af4357d_003FBar_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003AC_0021_003FUsers_003Fschaa_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fbd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpHeaders_002Ecs_002Fl_003AC_0021_003FUsers_003Fschaa_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F66e1248da26799c021f14a7e458d63250ea9fa0bcc979f1e5a1a43ca6f3cc7_003FHttpHeaders_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003AC_0021_003FUsers_003Fschaa_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F6354a7b35d7821629924d3676acd7e67a6f7f94343e0e66ec439aa2bd6ed5_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>

View file

@ -25,18 +25,6 @@ namespace Schaad.Accounting.Datasets
Status = status; Status = status;
} }
} }
public string GetCssAlertClass()
{
switch (Status)
{
case MessageStatus.Success: return "alert-success";
case MessageStatus.Info: return "alert-info";
case MessageStatus.Warning: return "alert-warning";
case MessageStatus.Error: return "alert-danger";
default: return "alert-success";
}
}
} }
public enum MessageStatus public enum MessageStatus

View file

@ -7,6 +7,7 @@
<FluentStack Class="main" Orientation="Orientation.Horizontal" Width="100%"> <FluentStack Class="main" Orientation="Orientation.Horizontal" Width="100%">
<NavMenu /> <NavMenu />
<FluentBodyContent Class="body-content"> <FluentBodyContent Class="body-content">
<FluentMessageBarProvider Section="MESSAGES_TOP" @rendermode="InteractiveServer" />
<div class="content"> <div class="content">
@Body @Body
</div> </div>
@ -21,6 +22,7 @@
</FluentLayout> </FluentLayout>
<FluentDialogProvider @rendermode="InteractiveServer"/> <FluentDialogProvider @rendermode="InteractiveServer"/>
<FluentToastProvider MaxToastCount="10" @rendermode="InteractiveServer" />
<div id="blazor-error-ui" data-nosnippet> <div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred. An unhandled error has occurred.

View file

@ -17,7 +17,3 @@ AccountingNext
@bind-SelectedOption="@selectedMandator" @bind-SelectedOption="@selectedMandator"
ValueChanged="MandatorChanged"/> ValueChanged="MandatorChanged"/>
</FluentStack> </FluentStack>
@code {
}

View file

@ -8,6 +8,9 @@ public partial class MyHeader : ComponentBase
[Inject] [Inject]
private ISettingsService settingsService { get; set; } = null!; private ISettingsService settingsService { get; set; } = null!;
[Inject]
NavigationManager navigationManager { get; set; } = null!;
private List<int> years = new (); private List<int> years = new ();
private int selectedYear; private int selectedYear;
@ -32,12 +35,20 @@ public partial class MyHeader : ComponentBase
public void YearChanged(string syear) public void YearChanged(string syear)
{ {
var year = int.Parse(syear); var year = int.Parse(syear);
settingsService.SetYear(year); if (selectedYear != year)
mandators = settingsService.GetMandators(year).ToList(); {
settingsService.SetYear(year);
mandators = settingsService.GetMandators(year).ToList();
navigationManager.NavigateTo(navigationManager.Uri, forceLoad: true);
}
} }
public void MandatorChanged(string mandator) public void MandatorChanged(string mandator)
{ {
settingsService.SetMandator(mandator); if (selectedMandator != mandator)
{
settingsService.SetMandator(mandator);
navigationManager.NavigateTo(navigationManager.Uri, forceLoad: true);
}
} }
} }

View file

@ -4,16 +4,14 @@
<FluentPaginator State="@pagination" /> <FluentPaginator State="@pagination" />
<div id="datagrid-container"> <div id="datagrid-container">
<FluentDataGrid Items="@TransactionList" Pagination="@pagination" RowSize="DataGridRowSize.Small" AutoItemsPerPage="true" Style="overflow-y:hidden;">
<FluentDataGrid Items="@TransactionList" Pagination="@pagination" RowSize="@rowSize" AutoItemsPerPage="true" Style="overflow-y:hidden;"> <PropertyColumn Title="Buchungsdatum" Width="170px" Property="@(p => p.BookingDate)" Format="dd.MM.yyyy" Sortable="true" IsDefaultSortColumn="true" InitialSortDirection="SortDirection.Descending" />
<PropertyColumn Title="Buchungsdatum" Width="100" Property="@(p => p.BookingDate)" Format="dd.MM.yyyy" Sortable="true" IsDefaultSortColumn="true" InitialSortDirection="SortDirection.Descending" /> <PropertyColumn Title="Valutadatum" Width="170px" Property="@(p => p.ValueDate)" Format="dd.MM.yyyy" Sortable="true" />
<PropertyColumn Title="Valutadatum" Width="100" Property="@(p => p.ValueDate)" Format="dd.MM.yyyy" Sortable="true" />
<PropertyColumn Title="Text" Property="@(p => p.Text)" /> <PropertyColumn Title="Text" Property="@(p => p.Text)" />
<PropertyColumn Title="Betrag" Width="100" Property="@(p => p.Value)" Format="C2" Align="Align.End"/> <PropertyColumn Title="Betrag" Width="150px" Property="@(p => p.Value)" Format="C2" Align="Align.End"/>
</FluentDataGrid> </FluentDataGrid>
</div> </div>
@code { @code {
DataGridRowSize rowSize = DataGridRowSize.Small;
PaginationState pagination = new PaginationState() { ItemsPerPage = 10 }; PaginationState pagination = new PaginationState() { ItemsPerPage = 10 };
} }

View file

@ -1,7 +1,44 @@
@page "/" @page "/"
<h3>Home</h3> @rendermode InteractiveServer
@foreach (var bankTransaction in bankTransactions) <PageTitle>Home</PageTitle>
{
<p>@bankTransaction.BankAccountNumber</p> <FluentInputFile @ref="@myFileByBuffer"
} AnchorId="MyUploadBuffer"
DragDropZoneVisible="false"
Mode="InputFileMode.Buffer"
Multiple="true"
MaximumFileSize="@(10 * 1024 * 1024)"
Accept=".zip, .xml"
OnProgressChange="@OnProgressChangeAsync"
OnCompleted="@OnCompletedAsync" />
<FluentProgress Visible="@(progressPercent > 0)" Min="0" Max="100" Value="@progressPercent" />
<FluentLabel Alignment="HorizontalAlignment.Center">
@progressTitle
</FluentLabel>
<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)">
Cancel
</FluentButton>
<div id="datagrid-container">
<FluentDataGrid Items="@bankTransactionsQueryable" RowSize="DataGridRowSize.Small" AutoItemsPerPage="true">
<ChildContent>
<PropertyColumn Title="Buchungsdatum" Width="170px" Property="@(p => p.BookingDate)" Format="dd.MM.yyyy" Sortable="true" IsDefaultSortColumn="true" InitialSortDirection="SortDirection.Descending"/>
<PropertyColumn Title="Valutadatum" Width="170px" Property="@(p => p.ValueDate)" Format="dd.MM.yyyy" Sortable="true"/>
<PropertyColumn Title="Text" Property="@(p => p.Text)"/>
<PropertyColumn Title="Betrag" Width="150px" Property="@(p => p.Value)" Format="C2" Align="Align.End"/>
</ChildContent>
<EmptyContent>
<FluentStack Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center" HorizontalAlignment="HorizontalAlignment.Center" HorizontalGap="10">
<FluentIcon Value="@(new Icons.Filled.Size24.Crown())" Color="@Color.Accent"/>
<div>Keine Daten vorhanden!</div>
</FluentStack>
</EmptyContent>
</FluentDataGrid>
</div>

View file

@ -1,4 +1,7 @@
using Microsoft.AspNetCore.Components; using System.IO.Compression;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models; using Schaad.Accounting.Models;
@ -9,11 +12,126 @@ public partial class Home : ComponentBase
[Inject] [Inject]
private IViewService viewService { get; set; } = null!; private IViewService viewService { get; set; } = null!;
private List<BankTransaction> bankTransactions = null!; [Inject]
private IToastService toastService { get; set; } = null!;
[Inject]
private IMessageService messageService { get; set; } = null!;
[Inject]
private ISettingsService settingsService { get; set; } = null!;
[Inject]
private IFileService fileService { get; set; } = null!;
private IQueryable<BankTransaction>? bankTransactionsQueryable;
FluentInputFile? myFileByBuffer;
int? progressPercent;
string? progressTitle;
bool IsCanceled;
Dictionary<int, string> Files = new();
protected override Task OnInitializedAsync() protected override Task OnInitializedAsync()
{ {
bankTransactions = viewService.GetOpenBankTransactionList(); bankTransactionsQueryable = viewService.GetOpenBankTransactionList().AsQueryable();
return base.OnInitializedAsync(); return base.OnInitializedAsync();
} }
async Task OnProgressChangeAsync(FluentInputFileEventArgs file)
{
progressPercent = file.ProgressPercent;
progressTitle = file.ProgressTitle;
// To cancel?
file.IsCancelled = IsCanceled;
// New file
if (!Files.ContainsKey(file.Index))
{
var localFile = Path.GetTempFileName() + file.Name;
Files.Add(file.Index, localFile);
}
// Write to the FileStream
await file.Buffer.AppendToFileAsync(Files[file.Index]);
}
private async Task OnCompletedAsync(IEnumerable<FluentInputFileEventArgs> files)
{
progressPercent = myFileByBuffer!.ProgressPercent;
progressTitle = myFileByBuffer!.ProgressTitle;
foreach (var file in Files)
{
var extension = Path.GetExtension(file.Value);
if (extension is ".xml")
{
await ImportXmlAndShowResultAsync(file.Value);
}
else if (extension is ".zip")
{
var unzippedFiles = UnzipAndListFiles(file.Value);
foreach (var unzippedFile in unzippedFiles)
{
await ImportXmlAndShowResultAsync(unzippedFile);
File.Delete(unzippedFile);
}
}
File.Delete(file.Value);
}
bankTransactionsQueryable = viewService.GetOpenBankTransactionList().AsQueryable();
}
private async Task ImportXmlAndShowResultAsync(string fileName)
{
var messages = fileService.ImportAccountStatementFile(fileName);
foreach (var message in messages)
{
await ShowImportResultAsync(message);
}
}
private IReadOnlyList<string> UnzipAndListFiles(string zipFilePath)
{
var tempExtractPath = Path.Combine(Path.GetTempPath(), Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
Directory.CreateDirectory(tempExtractPath);
ZipFile.ExtractToDirectory(zipFilePath, tempExtractPath);
return new List<string>(Directory.GetFiles(tempExtractPath, "*", SearchOption.AllDirectories));
}
private async Task ShowImportResultAsync(MessageDataset message)
{
var joinedDetails = string.Join(", ", message.Lines);
var text = $"{message.Title}: {joinedDetails}";
if (message.Status == MessageStatus.Success)
{
ShowToast(text, ToastIntent.Success);
}
else if (message.Status == MessageStatus.Info)
{
ShowToast(text, ToastIntent.Info);
}
else if (message.Status == MessageStatus.Warning)
{
ShowToast(text, ToastIntent.Warning);
}
else if (message.Status == MessageStatus.Error)
{
await ShowMessageAsync(text, MessageIntent.Error);
}
}
private async Task ShowMessageAsync(string message, MessageIntent intent)
{
await messageService.ShowMessageBarAsync(message, intent, "MESSAGES_TOP");
}
private void ShowToast(string message, ToastIntent intent)
{
toastService.ShowToast(intent, message, 3000);
}
} }