PR U-10: split Home into StatementImport and PendingBookings
Home shrinks from ~200-line codebehind + 100-line markup to a 10-line orchestrator holding a @ref to <PendingBookings/> and wiring <StatementImport OnImported="RefreshPendingAsync"/> to it. Also renames IsCanceled -> cancelRequested and clears the Files dictionary after each import batch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
0c48539ce6
commit
97bf85d6a8
6 changed files with 315 additions and 272 deletions
|
|
@ -1,102 +1,9 @@
|
||||||
@page "/"
|
@page "/"
|
||||||
|
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
<PageTitle>Home</PageTitle>
|
<PageTitle>Home</PageTitle>
|
||||||
|
|
||||||
<FluentInputFile @ref="@myFileByBuffer"
|
<StatementImport OnImported="RefreshPendingAsync"/>
|
||||||
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"/>
|
<PendingBookings @ref="pendingBookings"/>
|
||||||
<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>
|
|
||||||
|
|
||||||
@if (matchingBankTransactions == null || matchingBankTransactions.Count == 0)
|
|
||||||
{
|
|
||||||
<p>Keine Transaktionen vorhanden!</p>
|
|
||||||
<FluentEmoji Value="@(new Emojis.SmileysEmotion.Color.Default.MoneyMouthFace())" />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Buchungsdatum</th>
|
|
||||||
<th>Valutadatum</th>
|
|
||||||
<th>Zusatzinfo</th>
|
|
||||||
<th>Text</th>
|
|
||||||
<th>Konto</th>
|
|
||||||
<th>Betrag</th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var transaction in matchingBankTransactions)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div>@transaction.BookingDate.ToString("dd.MM.yyyy")</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div>@transaction.ValueDate.ToString("dd.MM.yyyy")</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..." readonly="@transaction.ReadOnly"/>
|
|
||||||
<datalist id="texts">
|
|
||||||
@foreach (var text in bookingTexts.OrderBy(t => t))
|
|
||||||
{
|
|
||||||
<option value="@text"></option>
|
|
||||||
}
|
|
||||||
</datalist>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@if (transaction.Value > 0)
|
|
||||||
{
|
|
||||||
<AccountSelector Accounts="accounts" @bind-AccountId="@transaction.OriginAccountId" />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<AccountSelector Accounts="accounts" @bind-AccountId="@transaction.TargetAccountId" />
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div>@Math.Abs(transaction.Value).ToFormattedString()</div>
|
|
||||||
</td>
|
|
||||||
<td style="align-content: center">
|
|
||||||
<FluentButton IconEnd="@(new Icons.Regular.Size16.SplitVertical())" Appearance="Appearance.Neutral"
|
|
||||||
OnClick="@(() => SplitBankTransactionAsync(transaction))" Disabled="@(transaction.Value > 0 || transaction.ReadOnly)" />
|
|
||||||
</td>
|
|
||||||
<td style="align-content: center">
|
|
||||||
<FluentButton IconEnd="@(new Icons.Regular.Size16.Save())" Appearance="Appearance.Neutral"
|
|
||||||
OnClick="@(() => BookBankTransactionAsync(transaction))"
|
|
||||||
Disabled="@transaction.ReadOnly" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,188 +1,16 @@
|
||||||
using System.IO.Compression;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
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;
|
namespace Schaad.Accounting.UI.Components.Pages;
|
||||||
|
|
||||||
public partial class Home : ComponentBase
|
public partial class Home : ComponentBase
|
||||||
{
|
{
|
||||||
[Inject]
|
private PendingBookings? pendingBookings;
|
||||||
private IViewService viewService { get; set; } = null!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private IToastService toastService { get; set; } = null!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private IMessageService messageService { get; set; } = null!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private IDialogService dialogService { get; set; } = null!;
|
|
||||||
|
|
||||||
[Inject]
|
private async Task RefreshPendingAsync()
|
||||||
private IFileService fileService { get; set; } = null!;
|
|
||||||
|
|
||||||
[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;
|
|
||||||
int? progressPercent;
|
|
||||||
string? progressTitle;
|
|
||||||
bool IsCanceled;
|
|
||||||
Dictionary<int, string> Files = new();
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
{
|
||||||
await LoadDataAsync();
|
if (pendingBookings is not null)
|
||||||
await base.OnInitializedAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadDataAsync()
|
|
||||||
{
|
|
||||||
matchingBankTransactions = await viewService.MatchOpenBankTransactionsAsync();
|
|
||||||
accounts = await viewService.GetAccountViewListAsync();
|
|
||||||
bookingTexts = (await bookingTextRepository.GetBookingTextListAsync()).Select(b => b.Text).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
await pendingBookings.RefreshAsync();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Imports may update the bank-account balance and add new bank transactions
|
|
||||||
// — reload everything so the pending list, account balances, and booking-text
|
|
||||||
// suggestions stay consistent.
|
|
||||||
await LoadDataAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ImportXmlAndShowResultAsync(string fileName)
|
|
||||||
{
|
|
||||||
var messages = await fileService.ImportAccountStatementFileAsync(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)
|
|
||||||
{
|
|
||||||
await ShowMessageAsync(text, MessageIntent.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
await transactionRepository.SaveTransactionAsync(transaction);
|
|
||||||
transaction.ReadOnly = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SplitBankTransactionAsync(Transaction transaction)
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
// Re-run the auto-matcher in-place so the newly created split
|
|
||||||
// transactions disappear from the pending list. No forceLoad — the
|
|
||||||
// SignalR circuit stays up and the scoped RepositoryCache survives.
|
|
||||||
await LoadDataAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
73
Schaad.Accounting.UI/Components/Pages/PendingBookings.razor
Normal file
73
Schaad.Accounting.UI/Components/Pages/PendingBookings.razor
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
@if (matchingBankTransactions is null || matchingBankTransactions.Count == 0)
|
||||||
|
{
|
||||||
|
<p>Keine Transaktionen vorhanden!</p>
|
||||||
|
<FluentEmoji Value="@(new Emojis.SmileysEmotion.Color.Default.MoneyMouthFace())" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Buchungsdatum</th>
|
||||||
|
<th>Valutadatum</th>
|
||||||
|
<th>Zusatzinfo</th>
|
||||||
|
<th>Text</th>
|
||||||
|
<th>Konto</th>
|
||||||
|
<th>Betrag</th>
|
||||||
|
<th></th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var transaction in matchingBankTransactions)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div>@transaction.BookingDate.ToString("dd.MM.yyyy")</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>@transaction.ValueDate.ToString("dd.MM.yyyy")</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..." readonly="@transaction.ReadOnly"/>
|
||||||
|
<datalist id="texts">
|
||||||
|
@foreach (var text in bookingTexts.OrderBy(t => t))
|
||||||
|
{
|
||||||
|
<option value="@text"></option>
|
||||||
|
}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (transaction.Value > 0)
|
||||||
|
{
|
||||||
|
<AccountSelector Accounts="accounts" @bind-AccountId="@transaction.OriginAccountId" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<AccountSelector Accounts="accounts" @bind-AccountId="@transaction.TargetAccountId" />
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>@Math.Abs(transaction.Value).ToFormattedString()</div>
|
||||||
|
</td>
|
||||||
|
<td style="align-content: center">
|
||||||
|
<FluentButton IconEnd="@(new Icons.Regular.Size16.SplitVertical())" Appearance="Appearance.Neutral"
|
||||||
|
OnClick="@(() => SplitBankTransactionAsync(transaction))" Disabled="@(transaction.Value > 0 || transaction.ReadOnly)" />
|
||||||
|
</td>
|
||||||
|
<td style="align-content: center">
|
||||||
|
<FluentButton IconEnd="@(new Icons.Regular.Size16.Save())" Appearance="Appearance.Neutral"
|
||||||
|
OnClick="@(() => BookBankTransactionAsync(transaction))"
|
||||||
|
Disabled="@transaction.ReadOnly" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.FluentUI.AspNetCore.Components;
|
||||||
|
using Schaad.Accounting.Interfaces;
|
||||||
|
using Schaad.Accounting.Models;
|
||||||
|
using Schaad.Accounting.UI.Components.Pages.Dialogs;
|
||||||
|
|
||||||
|
namespace Schaad.Accounting.UI.Components.Pages;
|
||||||
|
|
||||||
|
public partial class PendingBookings : ComponentBase
|
||||||
|
{
|
||||||
|
[Inject]
|
||||||
|
private IViewService viewService { get; set; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private ITransactionRepository transactionRepository { get; set; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IBookingTextRepository bookingTextRepository { get; set; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IDialogService dialogService { get; set; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IToastService toastService { get; set; } = null!;
|
||||||
|
|
||||||
|
private List<Transaction>? matchingBankTransactions;
|
||||||
|
private IReadOnlyList<Account> accounts = [];
|
||||||
|
private IReadOnlyList<string> bookingTexts = [];
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadDataAsync();
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Re-run the auto-matcher and reload accounts / booking texts. Called from
|
||||||
|
/// the parent (Home) after a statement import completes.
|
||||||
|
/// </summary>
|
||||||
|
public async Task RefreshAsync()
|
||||||
|
{
|
||||||
|
await LoadDataAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadDataAsync()
|
||||||
|
{
|
||||||
|
matchingBankTransactions = await viewService.MatchOpenBankTransactionsAsync();
|
||||||
|
accounts = await viewService.GetAccountViewListAsync();
|
||||||
|
bookingTexts = (await bookingTextRepository.GetBookingTextListAsync()).Select(b => b.Text).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BookBankTransactionAsync(Transaction transaction)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(transaction.Text) || string.IsNullOrEmpty(transaction.TargetAccountId))
|
||||||
|
{
|
||||||
|
toastService.ShowToast(ToastIntent.Error, "Transaktion kann wegen fehlenden Daten nicht gespeichert werden!", 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await transactionRepository.SaveTransactionAsync(transaction);
|
||||||
|
transaction.ReadOnly = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SplitBankTransactionAsync(Transaction transaction)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
await LoadDataAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Schaad.Accounting.UI/Components/Pages/StatementImport.razor
Normal file
26
Schaad.Accounting.UI/Components/Pages/StatementImport.razor
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
<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) => cancelRequested = true)">
|
||||||
|
Cancel
|
||||||
|
</FluentButton>
|
||||||
128
Schaad.Accounting.UI/Components/Pages/StatementImport.razor.cs
Normal file
128
Schaad.Accounting.UI/Components/Pages/StatementImport.razor.cs
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
using System.IO.Compression;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.FluentUI.AspNetCore.Components;
|
||||||
|
using Schaad.Accounting.Datasets;
|
||||||
|
using Schaad.Accounting.Interfaces;
|
||||||
|
|
||||||
|
namespace Schaad.Accounting.UI.Components.Pages;
|
||||||
|
|
||||||
|
public partial class StatementImport : ComponentBase
|
||||||
|
{
|
||||||
|
[Inject]
|
||||||
|
private IFileService fileService { get; set; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IToastService toastService { get; set; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IMessageService messageService { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fires after all uploaded files have been processed, so the parent can
|
||||||
|
/// refresh dependent views (e.g. re-run the auto-matcher on Home).
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback OnImported { get; set; }
|
||||||
|
|
||||||
|
private FluentInputFile? myFileByBuffer;
|
||||||
|
private int? progressPercent;
|
||||||
|
private string? progressTitle;
|
||||||
|
private bool cancelRequested;
|
||||||
|
private Dictionary<int, string> Files = new();
|
||||||
|
|
||||||
|
private async Task OnProgressChangeAsync(FluentInputFileEventArgs file)
|
||||||
|
{
|
||||||
|
progressPercent = file.ProgressPercent;
|
||||||
|
progressTitle = file.ProgressTitle;
|
||||||
|
|
||||||
|
file.IsCancelled = cancelRequested;
|
||||||
|
|
||||||
|
if (!Files.ContainsKey(file.Index))
|
||||||
|
{
|
||||||
|
var localFile = Path.GetTempFileName() + file.Name;
|
||||||
|
Files.Add(file.Index, localFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.Clear();
|
||||||
|
|
||||||
|
await OnImported.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ImportXmlAndShowResultAsync(string fileName)
|
||||||
|
{
|
||||||
|
var messages = await fileService.ImportAccountStatementFileAsync(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)
|
||||||
|
{
|
||||||
|
await ShowMessageAsync(text, MessageIntent.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue