Add transaction dialog and enhance transaction view

This commit is contained in:
Claudio Schaad 2025-05-05 21:08:06 +02:00
parent d21c49d486
commit ece9618f09
13 changed files with 240 additions and 19 deletions

View file

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

View file

@ -10,5 +10,6 @@ namespace Schaad.Accounting.Interfaces
void SaveTransaction(Transaction transaction); void SaveTransaction(Transaction transaction);
Transaction GetTransaction(string id); Transaction GetTransaction(string id);
void DeleteTransaction(string id);
} }
} }

View file

@ -5,7 +5,7 @@ using System.Linq;
namespace Schaad.Accounting.Models namespace Schaad.Accounting.Models
{ {
public class Transaction public record Transaction
{ {
public string Id { get; set; } public string Id { get; set; }
public string BankTransactionId { get; set; } public string BankTransactionId { get; set; }
@ -18,21 +18,26 @@ namespace Schaad.Accounting.Models
public string RelatedParty { get; set; } public string RelatedParty { get; set; }
[Display(Name = "Valuta")] [Display(Name = "Valuta")]
[Required]
public DateTime ValueDate { get; set; } public DateTime ValueDate { get; set; }
[Display(Name = "Buchung")] [Display(Name = "Buchung")]
public DateTime BookingDate { get; set; } public DateTime BookingDate { get; set; }
[Display(Name = "Buchungstext")] [Display(Name = "Buchungstext")]
[Required]
public string Text { get; set; } public string Text { get; set; }
[Display(Name = "Betrag")] [Display(Name = "Betrag")]
[Required]
public decimal Value { get; set; } public decimal Value { get; set; }
[Display(Name = "Habenkonto")] [Display(Name = "Habenkonto")]
[Required]
public string OriginAccountId { get; set; } public string OriginAccountId { get; set; }
[Display(Name = "Sollkonto")] [Display(Name = "Sollkonto")]
[Required]
public string TargetAccountId { get; set; } public string TargetAccountId { get; set; }
[Display(Name = "Währungskurs")] [Display(Name = "Währungskurs")]

View file

@ -76,5 +76,15 @@ namespace Schaad.Accounting.Repositories
return transaction; return transaction;
} }
/// <summary>
/// Delete transaction
/// </summary>
public void DeleteTransaction(string id)
{
var transactions = GetTransactionList();
transactions.RemoveAll(t => t.Id == id);
Save(transactions, TRANSACTIONS);
}
} }
} }

View file

@ -136,8 +136,7 @@ namespace Schaad.Accounting.Services
new TransactionDataset( new TransactionDataset(
t, t,
accountList.Single(a => a.Id == t.OriginAccountId), accountList.Single(a => a.Id == t.OriginAccountId),
accountList.Single(a => a.Id == t.TargetAccountId) accountList.Single(a => a.Id == t.TargetAccountId))
)
) )
.ToList(); .ToList();
@ -155,7 +154,7 @@ namespace Schaad.Accounting.Services
var transactionViewList = transactionList.Select( var transactionViewList = transactionList.Select(
t => t =>
new TransactionDataset( new TransactionDataset(
t, Prepare(t),
accountList.Single(a => a.Id == t.OriginAccountId), accountList.Single(a => a.Id == t.OriginAccountId),
accountList.Single(a => a.Id == t.TargetAccountId) accountList.Single(a => a.Id == t.TargetAccountId)
) )
@ -163,6 +162,18 @@ namespace Schaad.Accounting.Services
.ToList(); .ToList();
return transactionViewList; return transactionViewList;
Transaction Prepare(Transaction t)
{
var account = accountList.Single(a => a.Id == t.OriginAccountId);
if (account.Class == ClassIds.Activa && accountId == t.OriginAccountId )
{
return t with { Value = -t.Value };
}
return t;
}
} }

View file

@ -12,7 +12,7 @@ public partial class MyHeader : ComponentBase
private int selectedYear; private int selectedYear;
private List<string> mandators = new (); private List<string> mandators = new ();
private string selectedMandator; private string selectedMandator = null!;
protected override Task OnInitializedAsync() protected override Task OnInitializedAsync()
{ {

View file

@ -0,0 +1,56 @@
@using Schaad.Accounting.Models
@implements IDialogContentComponent<Schaad.Accounting.Models.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>
<EditForm EditContext="@editContext">
<DataAnnotationsValidator />
<FluentDatePicker @bind-Value="@SelectedValue" Label="Valuta" Style="width: 300px;"/>
<FluentSelect TOption="Account"
Label="von"
Items="@accounts"
Width="300px"
Height="250px"
Required="true"
Placeholder="Konto auswählen..."
OptionValue="@(p => p.Id)"
OptionText="@(p => p.Number + " / " + p.Name)"
@bind-Value="@Content.OriginAccountId" />
<FluentSelect TOption="Account"
Label="nach"
Items="@accounts"
Width="300px"
Height="250px"
Required="true"
Placeholder="Konto auswählen..."
OptionValue="@(p => p.Id)"
OptionText="@(p => p.Number + " / " + p.Name)"
@bind-Value="@Content.TargetAccountId" />
<FluentNumberField @bind-Value="@Content.Value" Label="Betrag" Style="width: 300px;"/>
<FluentTextField @bind-Value="@Content.Text" Label="Buchungstext" Style="width: 300px;" />
<div>
<FluentValidationSummary />
</div>
</EditForm>
</FluentDialogBody>
<FluentDialogFooter>
<FluentButton Appearance="Appearance.Neutral"
OnClick="@CancelAsync">
Abbrechen
</FluentButton>
<FluentButton Appearance="Appearance.Accent"
OnClick="@SaveAsync">
Speichern
</FluentButton>
</FluentDialogFooter>

View file

@ -0,0 +1,50 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.FluentUI.AspNetCore.Components;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.UI.Components.Pages.Dialogs;
public partial class TransactionDialog : 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 IViewService viewService { get; set; } = null!;
private IReadOnlyList<Account> accounts = [];
private DateTime? SelectedValue;
protected override void OnInitialized()
{
SelectedValue = Content.ValueDate;
editContext = new EditContext(Content);
accounts = viewService.GetAccountViewList();
}
private async Task SaveAsync()
{
if (editContext.Validate())
{
Content.ValueDate = SelectedValue!.Value;
Content.BookingDate = SelectedValue!.Value;
transactionRepository.SaveTransaction(Content);
await Dialog.CloseAsync(Content);
}
}
private async Task CancelAsync()
{
await Dialog.CancelAsync();
}
}

View file

@ -17,7 +17,7 @@
</div> </div>
<div style="margin-left: auto;"> <div style="margin-left: auto;">
<FluentBadge Appearance="Appearance.Neutral"> <FluentBadge Appearance="Appearance.Neutral">
@account.BalanceCHF.ToString("C2") @Math.Abs(account.BalanceCHF).ToString("C2")
</FluentBadge> </FluentBadge>
</div> </div>
</FluentStack> </FluentStack>
@ -29,9 +29,20 @@
</FluentNavMenu> </FluentNavMenu>
<FluentBodyContent> <FluentBodyContent>
<FluentPaginator State="@pagination" SummaryTemplate="@template" /> <FluentGrid Justify="JustifyContent.FlexStart">
<FluentGridItem xs="4" Class="align-content-center title">
@selectedAccount.Name
</FluentGridItem>
<FluentGridItem xs="4" Class="align-content-center">
Total @Math.Abs(selectedAccount.Balance).ToString("C2") in @transactionList?.Count() Transaktionen
</FluentGridItem>
<FluentGridItem xs="4" Class="align-content-center" Justify="JustifyContent.FlexEnd">
<FluentButton IconStart="@(new Icons.Regular.Size16.Add())" Appearance="Appearance.Neutral" OnClick="AddAsync">Neue Transaktion hinzufügen</FluentButton>
</FluentGridItem>
</FluentGrid>
<FluentDataGrid Items="@transactionList" Pagination="@pagination"> <FluentPaginator State="@pagination" SummaryTemplate="@template"/>
<FluentDataGrid Items="@transactionList" Pagination="@pagination" RowSize="DataGridRowSize.Medium">
<PropertyColumn Property="@(p => p.BookingDate)" Format="dd.MM.yyyy" Sortable="true" IsDefaultSortColumn="true" InitialSortDirection="SortDirection.Descending"/> <PropertyColumn Property="@(p => p.BookingDate)" Format="dd.MM.yyyy" Sortable="true" IsDefaultSortColumn="true" InitialSortDirection="SortDirection.Descending"/>
<PropertyColumn Property="@(p => p.ValueDate)" Format="dd.MM.yyyy" Sortable="true"/> <PropertyColumn Property="@(p => p.ValueDate)" Format="dd.MM.yyyy" Sortable="true"/>
<PropertyColumn Property="@(p => p.Text)"/> <PropertyColumn Property="@(p => p.Text)"/>
@ -39,11 +50,14 @@
@GetAccountName(context) @GetAccountName(context)
</TemplateColumn> </TemplateColumn>
<PropertyColumn Property="@(p => p.Value)" Format="C2" Align="Align.End"/> <PropertyColumn Property="@(p => p.Value)" Format="C2" Align="Align.End"/>
<TemplateColumn Align="Align.Center" Width="50px">
<FluentButton IconEnd="@(new Icons.Regular.Size16.Delete())" Appearance="Appearance.Neutral" OnClick="@(() => DeleteAsync(context.Id))"/>
</TemplateColumn>
<TemplateColumn Align="Align.Center" Width="50px">
<FluentButton IconEnd="@(new Icons.Regular.Size16.Edit())" Appearance="Appearance.Neutral" OnClick="@(() => EditAsync(context.Id))"/>
</TemplateColumn>
</FluentDataGrid> </FluentDataGrid>
</FluentBodyContent> </FluentBodyContent>
</FluentStack> </FluentStack>
@code @code

View file

@ -1,6 +1,9 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Schaad.Accounting.Datasets; using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces; 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;
@ -9,20 +12,29 @@ public partial class Transactions : ComponentBase
[Inject] [Inject]
private IViewService viewService { get; set; } = null!; private IViewService viewService { get; set; } = null!;
[Inject]
private ITransactionRepository transactionRepository { get; set; } = null!;
[Inject]
private IDialogService dialogService { get; set; } = null!;
private IReadOnlyList<AccountDataset> accounts = null!; private IReadOnlyList<AccountDataset> accounts = null!;
private IQueryable<TransactionDataset>? transactionList; private IQueryable<TransactionDataset>? transactionList;
private string? selectedAccountId; private string selectedAccountId = null!;
private AccountDataset selectedAccount = null!;
protected override Task OnInitializedAsync() protected override Task OnInitializedAsync()
{ {
accounts = viewService.GetAccountViewList(); accounts = viewService.GetAccountViewList();
ShowTransactions(accounts.First().Id);
return base.OnInitializedAsync(); return base.OnInitializedAsync();
} }
private void ShowTransactions(string accountId) private void ShowTransactions(string accountId)
{ {
selectedAccountId = accountId; selectedAccountId = accountId;
selectedAccount = accounts.Single(a => a.Id == accountId);
transactionList = viewService.GetTransactionViewList(accountId).AsQueryable(); transactionList = viewService.GetTransactionViewList(accountId).AsQueryable();
} }
@ -35,4 +47,57 @@ public partial class Transactions : ComponentBase
return transaction.OriginAccount.Name; return transaction.OriginAccount.Name;
} }
private async Task AddAsync()
{
var data = new Transaction
{
ValueDate = DateTime.Now,
BookingDate = DateTime.Now,
TargetAccountId = selectedAccountId
};
var dialog = await dialogService.ShowDialogAsync<TransactionDialog>(data, new DialogParameters()
{
Height = "500px",
Title = "Transaktion",
PreventDismissOnOverlayClick = true,
PreventScroll = true,
});
var result = await dialog.Result;
if (!result.Cancelled && result.Data != null)
{
transactionList = viewService.GetTransactionViewList(selectedAccountId).AsQueryable();
}
}
private async Task EditAsync(string id)
{
var data = transactionRepository.GetTransaction(id);
var dialog = await dialogService.ShowDialogAsync<TransactionDialog>(data, new DialogParameters()
{
Height = "500px",
Title = "Transaktion",
PreventDismissOnOverlayClick = true,
PreventScroll = true,
});
var result = await dialog.Result;
if (!result.Cancelled && result.Data != null)
{
transactionList = viewService.GetTransactionViewList(selectedAccountId).AsQueryable();
}
}
private async Task DeleteAsync(string id)
{
var transaction = transactionRepository.GetTransaction(id);
var dialog = await dialogService.ShowConfirmationAsync($"Transaction '{transaction.Text}' mit Betrag {transaction.Value} wirklich löschen?", "Ja", "Nein", "Transaktion löschen");
var result = await dialog.Result;
if (!result.Cancelled)
{
transactionRepository.DeleteTransaction(id);
transactionList = viewService.GetTransactionViewList(selectedAccountId).AsQueryable();
}
}
} }

View file

@ -0,0 +1,3 @@
.title {
font-size: 16pt;
}

View file

@ -1,6 +1,7 @@
@using System.ComponentModel.DataAnnotations @using System.ComponentModel.DataAnnotations
@using System.Net.Http @using System.Net.Http
@using System.Net.Http.Json @using System.Net.Http.Json
@using Microsoft.FluentUI.AspNetCore.Components.Extensions
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web

View file

@ -27,4 +27,9 @@
<PackageReference Include="Plotly.Blazor" Version="6.0.1" /> <PackageReference Include="Plotly.Blazor" Version="6.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="Components\Pages\Transactions.razor.css" />
<Content Include="Components\Pages\Transactions.razor.css" />
</ItemGroup>
</Project> </Project>