Skip to content

Commit

Permalink
feat(desktop): Add UI for loan payments
Browse files Browse the repository at this point in the history
  • Loading branch information
VMelnalksnis committed Feb 24, 2024
1 parent 9ef433d commit c019727
Show file tree
Hide file tree
Showing 31 changed files with 1,055 additions and 338 deletions.
7 changes: 7 additions & 0 deletions source/Gnomeshade.Avalonia.Core/AutoCompleteSelectors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Gnomeshade.Avalonia.Core.Reports.Aggregates;
using Gnomeshade.Avalonia.Core.Reports.Calculations;
using Gnomeshade.WebApi.Models.Accounts;
using Gnomeshade.WebApi.Models.Loans;
using Gnomeshade.WebApi.Models.Owners;
using Gnomeshade.WebApi.Models.Products;

Expand All @@ -18,12 +19,15 @@ internal static class AutoCompleteSelectors

internal static AutoCompleteSelector<object> Account { get; } = (_, item) => ((Account)item).Name;

/// <summary>Gets a delegate for formatting a counterparty in an <see cref="AutoCompleteBox"/>.</summary>
internal static AutoCompleteSelector<object> Counterparty { get; } = (_, item) => ((Counterparty)item).Name;

internal static AutoCompleteSelector<object> Category { get; } = (_, item) => ((Category)item).Name;

/// <summary>Gets a delegate for formatting a currency in an <see cref="AutoCompleteBox"/>.</summary>
internal static AutoCompleteSelector<object> Currency { get; } = (_, item) => ((Currency)item).AlphabeticCode;

/// <summary>Gets a delegate for formatting a owner in an <see cref="AutoCompleteBox"/>.</summary>
internal static AutoCompleteSelector<object> Owner { get; } = (_, item) => ((Owner)item).Name;

internal static AutoCompleteSelector<object> Product { get; } = (_, item) => ((Product)item).Name;
Expand All @@ -33,4 +37,7 @@ internal static class AutoCompleteSelectors
internal static AutoCompleteSelector<object> Aggregate { get; } = (_, item) => ((IAggregateFunction)item).Name;

internal static AutoCompleteSelector<object> Calculation { get; } = (_, item) => ((ICalculationFunction)item).Name;

/// <summary>Gets a delegate for formatting a loan in an <see cref="AutoCompleteBox"/>.</summary>
internal static AutoCompleteSelector<object> Loan { get; } = (_, item) => ((Loan)item).Name;
}
17 changes: 13 additions & 4 deletions source/Gnomeshade.Avalonia.Core/DesignTime/DesignTimeData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
using Gnomeshade.Avalonia.Core.Counterparties;
using Gnomeshade.Avalonia.Core.Help;
using Gnomeshade.Avalonia.Core.Imports;
using Gnomeshade.Avalonia.Core.Loans;
using Gnomeshade.Avalonia.Core.Loans.Migration;
using Gnomeshade.Avalonia.Core.Products;
using Gnomeshade.Avalonia.Core.Reports;
using Gnomeshade.Avalonia.Core.Transactions;
using Gnomeshade.Avalonia.Core.Transactions.Controls;
using Gnomeshade.Avalonia.Core.Transactions.Links;
using Gnomeshade.Avalonia.Core.Transactions.Loans;
using Gnomeshade.Avalonia.Core.Transactions.Loans.Migration;
using Gnomeshade.Avalonia.Core.Transactions.Purchases;
using Gnomeshade.Avalonia.Core.Transactions.Transfers;
using Gnomeshade.WebApi.Client;
Expand Down Expand Up @@ -150,12 +151,20 @@ public static class DesignTimeData
InitializeViewModel<LinkViewModel, LinkOverview, LinkUpsertionViewModel>(new(ActivityService, GnomeshadeClient, Guid.Empty));

/// <summary>Gets an instance of <see cref="LoanUpsertionViewModel"/> for use during design time.</summary>
public static LoanPaymentUpsertionViewModel LoanPaymentUpsertionViewModel { get; } =
InitializeViewModel(new LoanPaymentUpsertionViewModel(ActivityService, GnomeshadeClient, Guid.Empty, null));

/// <summary>Gets an instance of <see cref="LoanPaymentViewModel"/> for use during design time.</summary>
public static LoanPaymentViewModel LoanPaymentViewModel { get; } =
InitializeViewModel<LoanPaymentViewModel, LoanPaymentRow, LoanPaymentUpsertionViewModel>(new(ActivityService, GnomeshadeClient, Guid.Empty));

/// <summary>Gets an instance of <see cref="Loans.LoanUpsertionViewModel"/> for use during design time.</summary>
public static LoanUpsertionViewModel LoanUpsertionViewModel { get; } =
InitializeViewModel(new LoanUpsertionViewModel(ActivityService, GnomeshadeClient, Guid.Empty, null));
InitializeViewModel(new LoanUpsertionViewModel(ActivityService, GnomeshadeClient, null));

/// <summary>Gets an instance of <see cref="LoanViewModel"/> for use during design time.</summary>
/// <summary>Gets an instance of <see cref="Loans.LoanViewModel"/> for use during design time.</summary>
public static LoanViewModel LoanViewModel { get; } =
InitializeViewModel<LoanViewModel, LoanOverview, LoanUpsertionViewModel>(new(ActivityService, GnomeshadeClient, Guid.Empty));
InitializeViewModel<LoanViewModel, LoanRow, LoanUpsertionViewModel>(new(ActivityService, GnomeshadeClient));

/// <summary>Gets an instance of <see cref="CategoryReportViewModel"/> for use during design time.</summary>
public static CategoryReportViewModel CategoryReportViewModel { get; } =
Expand Down
44 changes: 44 additions & 0 deletions source/Gnomeshade.Avalonia.Core/Loans/LoanPaymentRow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2021 Valters Melnalksnis
// Licensed under the GNU Affero General Public License v3.0 or later.
// See LICENSE.txt file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Linq;

using Gnomeshade.WebApi.Models.Accounts;
using Gnomeshade.WebApi.Models.Loans;

namespace Gnomeshade.Avalonia.Core.Loans;

/// <summary>Overview of a single <see cref="LoanPayment"/>.</summary>
public sealed class LoanPaymentRow : PropertyChangedBase
{
/// <summary>Initializes a new instance of the <see cref="LoanPaymentRow"/> class.</summary>
/// <param name="payment">The payment which this row will represent.</param>
/// <param name="loans">All available loans.</param>
/// <param name="currencies">All available currencies.</param>
public LoanPaymentRow(LoanPayment payment, IEnumerable<Loan> loans, IEnumerable<Currency> currencies)
{
var loan = loans.Single(loan => loan.Id == payment.LoanId);
Loan = loan.Name;
Amount = payment.Amount;
Interest = payment.Interest;
Currency = currencies.Single(currency => currency.Id == loan.CurrencyId).AlphabeticCode;
Id = payment.Id;
}

/// <summary>Gets the name of the loan that this payment is a part of.</summary>
public string Loan { get; }

/// <summary>Gets the amount that was loaned or payed back.</summary>
public decimal Amount { get; }

/// <summary>Gets the interest amount of this loan payment.</summary>
public decimal Interest { get; }

/// <summary>Gets the alphabetic code of the currency of <see cref="Amount"/> and <see cref="Interest"/>.</summary>
public string Currency { get; }

internal Guid Id { get; }
}
66 changes: 66 additions & 0 deletions source/Gnomeshade.Avalonia.Core/Loans/LoanRow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2021 Valters Melnalksnis
// Licensed under the GNU Affero General Public License v3.0 or later.
// See LICENSE.txt file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Linq;

using Gnomeshade.WebApi.Models.Accounts;
using Gnomeshade.WebApi.Models.Loans;

namespace Gnomeshade.Avalonia.Core.Loans;

/// <summary>Overview of a single <see cref="Loan"/>.</summary>
public sealed class LoanRow : PropertyChangedBase
{
/// <summary>Initializes a new instance of the <see cref="LoanRow"/> class.</summary>
/// <param name="loan">The loan which this row will represent.</param>
/// <param name="counterparties">All available counterparties.</param>
/// <param name="currencies">All available currencies.</param>
/// <param name="payments">All available loan payments.</param>
public LoanRow(
Loan loan,
IReadOnlyCollection<Counterparty> counterparties,
IEnumerable<Currency> currencies,
IEnumerable<LoanPayment> payments)
{
Id = loan.Id;
Name = loan.Name;
Issuer = counterparties.Single(counterparty => counterparty.Id == loan.IssuingCounterpartyId).Name;
Receiver = counterparties.Single(counterparty => counterparty.Id == loan.ReceivingCounterpartyId).Name;
Principal = loan.Principal;
Currency = currencies.Single(currency => currency.Id == loan.CurrencyId).AlphabeticCode;

var loanPayments = payments.Where(payment => payment.LoanId == Id).ToArray();
ActualPrincipal = loanPayments.Where(payment => payment.Amount > 0).Sum(payment => payment.Amount);
PaidPrincipal = loanPayments.Where(payment => payment.Amount < 0).Sum(payment => -payment.Amount);
PaidInterest = loanPayments.Where(payment => payment.Amount < 0).Sum(payment => -payment.Interest);
}

/// <summary>Gets the name of the loan.</summary>
public string Name { get; }

/// <summary>Gets the name of the issuing counterparty.</summary>
public string Issuer { get; }

/// <summary>Gets the name of the receiving counterparty.</summary>
public string Receiver { get; }

/// <summary>Gets the amount of capital originally borrowed or invested.</summary>
public decimal Principal { get; }

/// <summary>Gets the currency of <see cref="Principal"/>.</summary>
public string Currency { get; }

/// <summary>Gets the actual amount borrow as indicated by loan payments.</summary>
public decimal ActualPrincipal { get; }

/// <summary>Gets the amount of <see cref="Principal"/> that has been paid back.</summary>
public decimal PaidPrincipal { get; }

/// <summary>Gets the amount of interest paid.</summary>
public decimal PaidInterest { get; }

internal Guid Id { get; }
}
126 changes: 126 additions & 0 deletions source/Gnomeshade.Avalonia.Core/Loans/LoanUpsertionViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2021 Valters Melnalksnis
// Licensed under the GNU Affero General Public License v3.0 or later.
// See LICENSE.txt file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using Avalonia.Controls;

using Gnomeshade.WebApi.Client;
using Gnomeshade.WebApi.Models.Accounts;
using Gnomeshade.WebApi.Models.Loans;
using Gnomeshade.WebApi.Models.Owners;

using PropertyChanged.SourceGenerator;

namespace Gnomeshade.Avalonia.Core.Loans;

/// <summary>Creates or updates a single loan.</summary>
public sealed partial class LoanUpsertionViewModel : UpsertionViewModel
{
/// <summary>Gets a collection of all owners.</summary>
[Notify(Setter.Private)]
private List<Owner> _owners = [];

/// <summary>Gets a collection of all counterparties.</summary>
[Notify(Setter.Private)]
private List<Counterparty> _counterparties = [];

/// <summary>Gets a collection of all currencies.</summary>
[Notify(Setter.Private)]
private List<Currency> _currencies = [];

/// <summary>Gets or sets the name of the loan.</summary>
[Notify]
private string? _name;

/// <summary>Gets or sets the owner of the loan.</summary>
[Notify]
private Owner? _owner;

/// <summary>Gets or sets the issuer of the loan.</summary>
[Notify]
private Counterparty? _issuer;

/// <summary>Gets or sets the receiver of the loan.</summary>
[Notify]
private Counterparty? _receiver;

/// <summary>Gets or sets the amount of capital originally borrowed or invested.</summary>
[Notify]
private decimal? _principal;

/// <summary>Gets or sets the currency of <see cref="Principal"/>.</summary>
[Notify]
private Currency? _currency;

/// <summary>Initializes a new instance of the <see cref="LoanUpsertionViewModel"/> class.</summary>
/// <param name="activityService">Service for indicating the activity of the application to the user.</param>
/// <param name="gnomeshadeClient">A strongly typed API client.</param>
/// <param name="id">The id of the loan to edit.</param>
public LoanUpsertionViewModel(IActivityService activityService, IGnomeshadeClient gnomeshadeClient, Guid? id)
: base(activityService, gnomeshadeClient)
{
Id = id;
}

/// <inheritdoc cref="AutoCompleteSelectors.Counterparty"/>
public AutoCompleteSelector<object> CounterpartySelector => AutoCompleteSelectors.Counterparty;

/// <inheritdoc cref="AutoCompleteSelectors.Currency"/>
public AutoCompleteSelector<object> CurrencySelector => AutoCompleteSelectors.Currency;

/// <inheritdoc cref="AutoCompleteSelectors.Owner"/>
public AutoCompleteSelector<object> OwnerSelector => AutoCompleteSelectors.Owner;

/// <inheritdoc />
public override bool CanSave =>
!string.IsNullOrWhiteSpace(Name) &&
Issuer is not null &&
Receiver is not null &&
Issuer.Id != Receiver.Id &&
Principal is not null &&
Currency is not null;

/// <inheritdoc />
protected override async Task<Guid> SaveValidatedAsync()
{
var id = Id ?? Guid.NewGuid();
var loan = new LoanCreation
{
OwnerId = Owner?.Id,
Name = Name!,
IssuingCounterpartyId = Issuer?.Id,
ReceivingCounterpartyId = Receiver?.Id,
Principal = Principal,
CurrencyId = Currency?.Id,
};

await GnomeshadeClient.PutLoanAsync(id, loan);
return id;
}

/// <inheritdoc />
protected override async Task Refresh()
{
Owners = await GnomeshadeClient.GetOwnersAsync();
Counterparties = await GnomeshadeClient.GetCounterpartiesAsync();
Currencies = await GnomeshadeClient.GetCurrenciesAsync();

if (Id is not { } id)
{
return;
}

var loan = await GnomeshadeClient.GetLoanAsync(id);
Owner = Owners.Single(owner => owner.Id == loan.OwnerId);
Name = loan.Name;
Issuer = Counterparties.Single(counterparty => counterparty.Id == loan.IssuingCounterpartyId);
Receiver = Counterparties.Single(counterparty => counterparty.Id == loan.ReceivingCounterpartyId);
Principal = loan.Principal;
Currency = Currencies.Single(currency => currency.Id == loan.CurrencyId);
}
}
70 changes: 70 additions & 0 deletions source/Gnomeshade.Avalonia.Core/Loans/LoanViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2021 Valters Melnalksnis
// Licensed under the GNU Affero General Public License v3.0 or later.
// See LICENSE.txt file in the project root for full license information.

using System.Linq;
using System.Threading.Tasks;

using Gnomeshade.WebApi.Client;

namespace Gnomeshade.Avalonia.Core.Loans;

/// <summary>An overview of all loans.</summary>
public sealed class LoanViewModel : OverviewViewModel<LoanRow, LoanUpsertionViewModel>
{
private readonly IGnomeshadeClient _gnomeshadeClient;

private LoanUpsertionViewModel _details;

/// <summary>Initializes a new instance of the <see cref="LoanViewModel"/> class.</summary>
/// <param name="activityService">Service for indicating the activity of the application to the user.</param>
/// <param name="gnomeshadeClient">A strongly typed API client.</param>
public LoanViewModel(IActivityService activityService, IGnomeshadeClient gnomeshadeClient)
: base(activityService)
{
_gnomeshadeClient = gnomeshadeClient;

_details = new(activityService, _gnomeshadeClient, null);
}

/// <inheritdoc />
public override LoanUpsertionViewModel Details
{
get => _details;
set => SetAndNotify(ref _details, value);
}

/// <inheritdoc />
public override async Task UpdateSelection()
{
Details = new(ActivityService, _gnomeshadeClient, Selected?.Id);
await Details.RefreshAsync();
}

/// <inheritdoc />
protected override async Task DeleteAsync(LoanRow row)
{
await _gnomeshadeClient.DeleteLoanAsync(row.Id);
await RefreshAsync();
}

/// <inheritdoc />
protected override async Task Refresh()
{
var (loans, loanPayments, counterparties, currencies) = await (
_gnomeshadeClient.GetLoansAsync(),
_gnomeshadeClient.GetLoanPaymentsAsync(),
_gnomeshadeClient.GetCounterpartiesAsync(),
_gnomeshadeClient.GetCurrenciesAsync())
.WhenAll();

var loanOverviews = loans
.Select(loan => new LoanRow(loan, counterparties, currencies, loanPayments))
.ToArray();

Rows = new(loanOverviews);

Details = new(ActivityService, _gnomeshadeClient, Selected?.Id);
await Details.RefreshAsync();
}
}
Loading

0 comments on commit c019727

Please sign in to comment.