diff --git a/source/Gnomeshade.Avalonia.Core/AutoCompleteSelectors.cs b/source/Gnomeshade.Avalonia.Core/AutoCompleteSelectors.cs index c0b731323..ca5b77707 100644 --- a/source/Gnomeshade.Avalonia.Core/AutoCompleteSelectors.cs +++ b/source/Gnomeshade.Avalonia.Core/AutoCompleteSelectors.cs @@ -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; @@ -18,12 +19,15 @@ internal static class AutoCompleteSelectors internal static AutoCompleteSelector Account { get; } = (_, item) => ((Account)item).Name; + /// Gets a delegate for formatting a counterparty in an . internal static AutoCompleteSelector Counterparty { get; } = (_, item) => ((Counterparty)item).Name; internal static AutoCompleteSelector Category { get; } = (_, item) => ((Category)item).Name; + /// Gets a delegate for formatting a currency in an . internal static AutoCompleteSelector Currency { get; } = (_, item) => ((Currency)item).AlphabeticCode; + /// Gets a delegate for formatting a owner in an . internal static AutoCompleteSelector Owner { get; } = (_, item) => ((Owner)item).Name; internal static AutoCompleteSelector Product { get; } = (_, item) => ((Product)item).Name; @@ -33,4 +37,7 @@ internal static class AutoCompleteSelectors internal static AutoCompleteSelector Aggregate { get; } = (_, item) => ((IAggregateFunction)item).Name; internal static AutoCompleteSelector Calculation { get; } = (_, item) => ((ICalculationFunction)item).Name; + + /// Gets a delegate for formatting a loan in an . + internal static AutoCompleteSelector Loan { get; } = (_, item) => ((Loan)item).Name; } diff --git a/source/Gnomeshade.Avalonia.Core/DesignTime/DesignTimeData.cs b/source/Gnomeshade.Avalonia.Core/DesignTime/DesignTimeData.cs index 8a60b421f..55a67993e 100644 --- a/source/Gnomeshade.Avalonia.Core/DesignTime/DesignTimeData.cs +++ b/source/Gnomeshade.Avalonia.Core/DesignTime/DesignTimeData.cs @@ -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; @@ -150,12 +151,20 @@ public static class DesignTimeData InitializeViewModel(new(ActivityService, GnomeshadeClient, Guid.Empty)); /// Gets an instance of for use during design time. + public static LoanPaymentUpsertionViewModel LoanPaymentUpsertionViewModel { get; } = + InitializeViewModel(new LoanPaymentUpsertionViewModel(ActivityService, GnomeshadeClient, Guid.Empty, null)); + + /// Gets an instance of for use during design time. + public static LoanPaymentViewModel LoanPaymentViewModel { get; } = + InitializeViewModel(new(ActivityService, GnomeshadeClient, Guid.Empty)); + + /// Gets an instance of for use during design time. public static LoanUpsertionViewModel LoanUpsertionViewModel { get; } = - InitializeViewModel(new LoanUpsertionViewModel(ActivityService, GnomeshadeClient, Guid.Empty, null)); + InitializeViewModel(new LoanUpsertionViewModel(ActivityService, GnomeshadeClient, null)); - /// Gets an instance of for use during design time. + /// Gets an instance of for use during design time. public static LoanViewModel LoanViewModel { get; } = - InitializeViewModel(new(ActivityService, GnomeshadeClient, Guid.Empty)); + InitializeViewModel(new(ActivityService, GnomeshadeClient)); /// Gets an instance of for use during design time. public static CategoryReportViewModel CategoryReportViewModel { get; } = diff --git a/source/Gnomeshade.Avalonia.Core/Loans/LoanPaymentRow.cs b/source/Gnomeshade.Avalonia.Core/Loans/LoanPaymentRow.cs new file mode 100644 index 000000000..f6767133e --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Loans/LoanPaymentRow.cs @@ -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; + +/// Overview of a single . +public sealed class LoanPaymentRow : PropertyChangedBase +{ + /// Initializes a new instance of the class. + /// The payment which this row will represent. + /// All available loans. + /// All available currencies. + public LoanPaymentRow(LoanPayment payment, IEnumerable loans, IEnumerable 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; + } + + /// Gets the name of the loan that this payment is a part of. + public string Loan { get; } + + /// Gets the amount that was loaned or payed back. + public decimal Amount { get; } + + /// Gets the interest amount of this loan payment. + public decimal Interest { get; } + + /// Gets the alphabetic code of the currency of and . + public string Currency { get; } + + internal Guid Id { get; } +} diff --git a/source/Gnomeshade.Avalonia.Core/Loans/LoanRow.cs b/source/Gnomeshade.Avalonia.Core/Loans/LoanRow.cs new file mode 100644 index 000000000..ac969a327 --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Loans/LoanRow.cs @@ -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; + +/// Overview of a single . +public sealed class LoanRow : PropertyChangedBase +{ + /// Initializes a new instance of the class. + /// The loan which this row will represent. + /// All available counterparties. + /// All available currencies. + /// All available loan payments. + public LoanRow( + Loan loan, + IReadOnlyCollection counterparties, + IEnumerable currencies, + IEnumerable 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); + } + + /// Gets the name of the loan. + public string Name { get; } + + /// Gets the name of the issuing counterparty. + public string Issuer { get; } + + /// Gets the name of the receiving counterparty. + public string Receiver { get; } + + /// Gets the amount of capital originally borrowed or invested. + public decimal Principal { get; } + + /// Gets the currency of . + public string Currency { get; } + + /// Gets the actual amount borrow as indicated by loan payments. + public decimal ActualPrincipal { get; } + + /// Gets the amount of that has been paid back. + public decimal PaidPrincipal { get; } + + /// Gets the amount of interest paid. + public decimal PaidInterest { get; } + + internal Guid Id { get; } +} diff --git a/source/Gnomeshade.Avalonia.Core/Loans/LoanUpsertionViewModel.cs b/source/Gnomeshade.Avalonia.Core/Loans/LoanUpsertionViewModel.cs new file mode 100644 index 000000000..30ae000ed --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Loans/LoanUpsertionViewModel.cs @@ -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; + +/// Creates or updates a single loan. +public sealed partial class LoanUpsertionViewModel : UpsertionViewModel +{ + /// Gets a collection of all owners. + [Notify(Setter.Private)] + private List _owners = []; + + /// Gets a collection of all counterparties. + [Notify(Setter.Private)] + private List _counterparties = []; + + /// Gets a collection of all currencies. + [Notify(Setter.Private)] + private List _currencies = []; + + /// Gets or sets the name of the loan. + [Notify] + private string? _name; + + /// Gets or sets the owner of the loan. + [Notify] + private Owner? _owner; + + /// Gets or sets the issuer of the loan. + [Notify] + private Counterparty? _issuer; + + /// Gets or sets the receiver of the loan. + [Notify] + private Counterparty? _receiver; + + /// Gets or sets the amount of capital originally borrowed or invested. + [Notify] + private decimal? _principal; + + /// Gets or sets the currency of . + [Notify] + private Currency? _currency; + + /// Initializes a new instance of the class. + /// Service for indicating the activity of the application to the user. + /// A strongly typed API client. + /// The id of the loan to edit. + public LoanUpsertionViewModel(IActivityService activityService, IGnomeshadeClient gnomeshadeClient, Guid? id) + : base(activityService, gnomeshadeClient) + { + Id = id; + } + + /// + public AutoCompleteSelector CounterpartySelector => AutoCompleteSelectors.Counterparty; + + /// + public AutoCompleteSelector CurrencySelector => AutoCompleteSelectors.Currency; + + /// + public AutoCompleteSelector OwnerSelector => AutoCompleteSelectors.Owner; + + /// + 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; + + /// + protected override async Task 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; + } + + /// + 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); + } +} diff --git a/source/Gnomeshade.Avalonia.Core/Loans/LoanViewModel.cs b/source/Gnomeshade.Avalonia.Core/Loans/LoanViewModel.cs new file mode 100644 index 000000000..0386db4a9 --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Loans/LoanViewModel.cs @@ -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; + +/// An overview of all loans. +public sealed class LoanViewModel : OverviewViewModel +{ + private readonly IGnomeshadeClient _gnomeshadeClient; + + private LoanUpsertionViewModel _details; + + /// Initializes a new instance of the class. + /// Service for indicating the activity of the application to the user. + /// A strongly typed API client. + public LoanViewModel(IActivityService activityService, IGnomeshadeClient gnomeshadeClient) + : base(activityService) + { + _gnomeshadeClient = gnomeshadeClient; + + _details = new(activityService, _gnomeshadeClient, null); + } + + /// + public override LoanUpsertionViewModel Details + { + get => _details; + set => SetAndNotify(ref _details, value); + } + + /// + public override async Task UpdateSelection() + { + Details = new(ActivityService, _gnomeshadeClient, Selected?.Id); + await Details.RefreshAsync(); + } + + /// + protected override async Task DeleteAsync(LoanRow row) + { + await _gnomeshadeClient.DeleteLoanAsync(row.Id); + await RefreshAsync(); + } + + /// + 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(); + } +} diff --git a/source/Gnomeshade.Avalonia.Core/Loans/Migration/LegacyLoanRow.cs b/source/Gnomeshade.Avalonia.Core/Loans/Migration/LegacyLoanRow.cs new file mode 100644 index 000000000..7c02329c8 --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Loans/Migration/LegacyLoanRow.cs @@ -0,0 +1,48 @@ +// 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.Transactions; + +namespace Gnomeshade.Avalonia.Core.Loans.Migration; + +/// Overview of a single . +public sealed class LegacyLoanRow : PropertyChangedBase +{ + /// Initializes a new instance of the class. + /// The loan which this row represents. + /// A collection of all available counterparties. + /// A collection of all available currencies. +#pragma warning disable CS0612 // Type or member is obsolete + public LegacyLoanRow( + LegacyLoan loan, + IReadOnlyCollection counterparties, + IEnumerable currencies) +#pragma warning restore CS0612 // Type or member is obsolete + { + Issuer = counterparties.Single(counterparty => counterparty.Id == loan.IssuingCounterpartyId).Name; + Receiver = counterparties.Single(counterparty => counterparty.Id == loan.ReceivingCounterpartyId).Name; + Amount = loan.Amount; + Currency = currencies.Single(currency => currency.Id == loan.CurrencyId).AlphabeticCode; + Id = loan.Id; + } + + /// Gets the name of the issuing counterparty of the loan. + public string Issuer { get; } + + /// Gets the name of the issuing counterparty of the loan. + public string Receiver { get; } + + /// Gets the amount loaned or paid back. + public decimal Amount { get; } + + /// Gets the currency of . + public string Currency { get; } + + internal Guid Id { get; } +} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Loans/Migration/LoanCounterpartyComparer.cs b/source/Gnomeshade.Avalonia.Core/Loans/Migration/LoanCounterpartyComparer.cs similarity index 83% rename from source/Gnomeshade.Avalonia.Core/Transactions/Loans/Migration/LoanCounterpartyComparer.cs rename to source/Gnomeshade.Avalonia.Core/Loans/Migration/LoanCounterpartyComparer.cs index 37b0637e7..5e4a53b6a 100644 --- a/source/Gnomeshade.Avalonia.Core/Transactions/Loans/Migration/LoanCounterpartyComparer.cs +++ b/source/Gnomeshade.Avalonia.Core/Loans/Migration/LoanCounterpartyComparer.cs @@ -7,12 +7,13 @@ using Gnomeshade.WebApi.Models.Transactions; -namespace Gnomeshade.Avalonia.Core.Transactions.Loans.Migration; +namespace Gnomeshade.Avalonia.Core.Loans.Migration; /// -internal sealed class LoanCounterpartyComparer : IEqualityComparer +[Obsolete] +internal sealed class LoanCounterpartyComparer : IEqualityComparer { - public bool Equals(Loan? x, Loan? y) + public bool Equals(LegacyLoan? x, LegacyLoan? y) { if (ReferenceEquals(x, y)) { @@ -29,7 +30,7 @@ public bool Equals(Loan? x, Loan? y) (x.IssuingCounterpartyId == y.ReceivingCounterpartyId && x.ReceivingCounterpartyId == y.IssuingCounterpartyId); } - public int GetHashCode(Loan obj) + public int GetHashCode(LegacyLoan obj) { return HashCode.Combine(obj.IssuingCounterpartyId, obj.ReceivingCounterpartyId); } diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Loans/Migration/LoanMigrationRow.cs b/source/Gnomeshade.Avalonia.Core/Loans/Migration/LoanMigrationRow.cs similarity index 88% rename from source/Gnomeshade.Avalonia.Core/Transactions/Loans/Migration/LoanMigrationRow.cs rename to source/Gnomeshade.Avalonia.Core/Loans/Migration/LoanMigrationRow.cs index 19553ea2d..ce581dd78 100644 --- a/source/Gnomeshade.Avalonia.Core/Transactions/Loans/Migration/LoanMigrationRow.cs +++ b/source/Gnomeshade.Avalonia.Core/Loans/Migration/LoanMigrationRow.cs @@ -3,8 +3,9 @@ // See LICENSE.txt file in the project root for full license information. using System.Collections.Generic; +using System.Linq; -namespace Gnomeshade.Avalonia.Core.Transactions.Loans.Migration; +namespace Gnomeshade.Avalonia.Core.Loans.Migration; /// Row for displaying how a loan will look after migrating. public sealed class LoanMigrationRow : PropertyChangedBase @@ -20,6 +21,7 @@ public LoanMigrationRow(string name, string issuer, string receiver, ListGets the receiver of the loan. public string Receiver { get; } + /// Gets the current loaned total. + public decimal Total { get; } + /// Gets the loan payment amounts. public List Amounts { get; } diff --git a/source/Gnomeshade.Avalonia.Core/Loans/Migration/LoanMigrationViewModel.cs b/source/Gnomeshade.Avalonia.Core/Loans/Migration/LoanMigrationViewModel.cs new file mode 100644 index 000000000..8adfdafb2 --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Loans/Migration/LoanMigrationViewModel.cs @@ -0,0 +1,175 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Gnomeshade.WebApi.Client; +using Gnomeshade.WebApi.Models.Loans; + +using NodaTime; + +using PropertyChanged.SourceGenerator; + +namespace Gnomeshade.Avalonia.Core.Loans.Migration; + +/// Overview of loans to be migrated to named loans/loan payments. +public sealed partial class LoanMigrationViewModel : ViewModelBase +{ + private readonly IGnomeshadeClient _gnomeshadeClient; + + /// Gets a collection of all current loans. + [Notify(Setter.Private)] + private List _loans = []; + + /// Gets a collection of all loan payments that will be created. + [Notify(Setter.Private)] + private List _migratedLoans = []; + + /// Initializes a new instance of the class. + /// Service for indicating the activity of the application to the user. + /// A strongly typed API client. + public LoanMigrationViewModel(IActivityService activityService, IGnomeshadeClient gnomeshadeClient) + : base(activityService) + { + _gnomeshadeClient = gnomeshadeClient; + } + + /// Migrate legacy loans to loans/loan payments. + /// A representing the asynchronous operation. + public async Task MigrateLoans() + { + using var activity = BeginActivity("Migrating loans"); + +#pragma warning disable CS0612 // Type or member is obsolete + var legacyLoans = await _gnomeshadeClient.GetLegacyLoans(); +#pragma warning restore CS0612 // Type or member is obsolete + var counterparties = await _gnomeshadeClient.GetCounterpartiesAsync(); + var transactions = await _gnomeshadeClient.GetDetailedTransactionsAsync(new(Instant.MinValue, Instant.MaxValue)); + var currencies = await _gnomeshadeClient.GetCurrenciesAsync(); + + var loansToCreate = legacyLoans + .OrderBy(loan => + { + var transaction = transactions.Single(transaction => transaction.Id == loan.TransactionId); + return transaction.ValuedAt ?? transaction.BookedAt; + }) +#pragma warning disable CS0612 // Type or member is obsolete + .GroupBy(loan => loan, new LoanCounterpartyComparer()) +#pragma warning restore CS0612 // Type or member is obsolete + .Select(grouping => + { + var counterparty1 = + counterparties.Single(counterparty => counterparty.Id == grouping.Key.IssuingCounterpartyId); + var counterparty2 = + counterparties.Single(counterparty => counterparty.Id == grouping.Key.ReceivingCounterpartyId); + return (counterparty1, counterparty2, grouping.ToArray()); + }) + .SelectMany(loanTuple => + { + var issuer = loanTuple.Item3.First().IssuingCounterpartyId == loanTuple.counterparty1.Id + ? loanTuple.counterparty1 + : loanTuple.counterparty2; + + var receiver = loanTuple.counterparty1 == issuer + ? loanTuple.counterparty2 + : loanTuple.counterparty1; + + return loanTuple.Item3.GroupBy(loan => loan.CurrencyId).Select(grouping => + { + var currency = currencies.Single(currency => currency.Id == grouping.Key); + var name = $"{issuer.Name} loan to {receiver.Name} ({currency.AlphabeticCode})"; + + var payments = grouping.Select(loan => new LoanPaymentCreation + { + TransactionId = loan.TransactionId, + Amount = loan.Amount, + Interest = 0, + }).ToArray(); + + var loanCreation = new LoanCreation + { + Name = name, + IssuingCounterpartyId = issuer.Id, + ReceivingCounterpartyId = receiver.Id, + Principal = payments.Sum(payment => payment.Amount), + CurrencyId = currency.Id, + }; + + return (loanCreation, payments); + }); + }); + + foreach (var (loanCreation, paymentCreations) in loansToCreate) + { + var loanId = await _gnomeshadeClient.CreateLoanAsync(loanCreation); + foreach (var paymentCreation in paymentCreations) + { + await _gnomeshadeClient.CreateLoanPaymentAsync(paymentCreation with { LoanId = loanId }); + } + } + } + + /// Deletes all legacy loans. + /// A representing the asynchronous operation. + public async Task DeleteLegacyLoans() + { + using var activity = BeginActivity("Deleting legacy loans"); + + foreach (var legacyLoanRow in Loans) + { +#pragma warning disable CS0612 // Type or member is obsolete + await _gnomeshadeClient.DeleteLegacyLoan(legacyLoanRow.Id); +#pragma warning restore CS0612 // Type or member is obsolete + } + } + + /// + protected override async Task Refresh() + { +#pragma warning disable CS0612 // Type or member is obsolete + var loans = await _gnomeshadeClient.GetLegacyLoans(); + var counterparties = await _gnomeshadeClient.GetCounterpartiesAsync(); + var transactions = await _gnomeshadeClient.GetDetailedTransactionsAsync(new(Instant.MinValue, Instant.MaxValue)); + var currencies = await _gnomeshadeClient.GetCurrenciesAsync(); + + MigratedLoans = loans + .OrderBy(loan => + { + var transaction = transactions.Single(transaction => transaction.Id == loan.TransactionId); + return transaction.ValuedAt ?? transaction.BookedAt; + }) + .GroupBy(loan => loan, new LoanCounterpartyComparer()) + .Select(grouping => + { + var counterparty1 = counterparties.Single(counterparty => counterparty.Id == grouping.Key.IssuingCounterpartyId); + var counterparty2 = counterparties.Single(counterparty => counterparty.Id == grouping.Key.ReceivingCounterpartyId); + return (counterparty1, counterparty2, grouping.ToArray()); + }) + .SelectMany(loanTuple => + { + var issuer = loanTuple.Item3.First().IssuingCounterpartyId == loanTuple.counterparty1.Id + ? loanTuple.counterparty1 + : loanTuple.counterparty2; + + var receiver = loanTuple.counterparty1 == issuer + ? loanTuple.counterparty2 + : loanTuple.counterparty1; + + return loanTuple.Item3.GroupBy(loan => loan.CurrencyId).Select(grouping => + { + var currency = currencies.Single(currency => currency.Id == grouping.Key); + var name = $"{issuer.Name} loan to {receiver.Name} ({currency.AlphabeticCode})"; + return new LoanMigrationRow(name, issuer.Name, receiver.Name, grouping.Select(loan => loan.Amount).ToList(), currency.AlphabeticCode); + }); + }) + .ToList(); +#pragma warning restore CS0612 // Type or member is obsolete + + Loans = loans + .Select(loan => new LegacyLoanRow(loan, counterparties, currencies)) + .ToList(); + } +} diff --git a/source/Gnomeshade.Avalonia.Core/MainWindowViewModel.cs b/source/Gnomeshade.Avalonia.Core/MainWindowViewModel.cs index 4e282b7e9..2679706d8 100644 --- a/source/Gnomeshade.Avalonia.Core/MainWindowViewModel.cs +++ b/source/Gnomeshade.Avalonia.Core/MainWindowViewModel.cs @@ -21,13 +21,14 @@ using Gnomeshade.Avalonia.Core.Products; using Gnomeshade.Avalonia.Core.Reports; using Gnomeshade.Avalonia.Core.Transactions; -using Gnomeshade.Avalonia.Core.Transactions.Loans.Migration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using PropertyChanged.SourceGenerator; +using LoanMigrationViewModel = Gnomeshade.Avalonia.Core.Loans.Migration.LoanMigrationViewModel; + namespace Gnomeshade.Avalonia.Core; /// A container view which manages navigation and the currently active view. @@ -170,6 +171,10 @@ public void SwitchToSetup() /// A representing the asynchronous operation. public Task SwitchToTransactionOverviewAsync() => SwitchTo(); + /// Switches to . + /// A representing the asynchronous operation. + public Task SwitchToLoanOverviewAsync() => SwitchTo(); + /// Switches to . /// A representing the asynchronous operation. public Task SwitchToLoanMigrationAsync() => SwitchTo(); diff --git a/source/Gnomeshade.Avalonia.Core/ServiceCollectionExtensions.cs b/source/Gnomeshade.Avalonia.Core/ServiceCollectionExtensions.cs index d74beaaee..329e4531b 100644 --- a/source/Gnomeshade.Avalonia.Core/ServiceCollectionExtensions.cs +++ b/source/Gnomeshade.Avalonia.Core/ServiceCollectionExtensions.cs @@ -9,13 +9,16 @@ using Gnomeshade.Avalonia.Core.Counterparties; using Gnomeshade.Avalonia.Core.Help; using Gnomeshade.Avalonia.Core.Imports; +using Gnomeshade.Avalonia.Core.Loans; using Gnomeshade.Avalonia.Core.Products; using Gnomeshade.Avalonia.Core.Reports; using Gnomeshade.Avalonia.Core.Transactions; -using Gnomeshade.Avalonia.Core.Transactions.Loans.Migration; +using Gnomeshade.Avalonia.Core.Transactions.Loans; using Microsoft.Extensions.DependencyInjection; +using LoanMigrationViewModel = Gnomeshade.Avalonia.Core.Loans.Migration.LoanMigrationViewModel; + namespace Gnomeshade.Avalonia.Core; /// Methods for configuring needed services in . @@ -48,5 +51,7 @@ public static IServiceCollection AddViewModels(this IServiceCollection serviceCo .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton(); } diff --git a/source/Gnomeshade.Avalonia.Core/TaskExtensions.cs b/source/Gnomeshade.Avalonia.Core/TaskExtensions.cs new file mode 100644 index 000000000..42499bbc8 --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/TaskExtensions.cs @@ -0,0 +1,34 @@ +// 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.Threading.Tasks; + +namespace Gnomeshade.Avalonia.Core; + +internal static class TaskExtensions +{ + internal static async Task<(T1 Result1, T2 Result2)> WhenAll( + this (Task Task1, Task Task2) tasks) + { + var (task1, task2) = tasks; + await Task.WhenAll(task1, task2); + return (task1.Result, task2.Result); + } + + internal static async Task<(T1 Result1, T2 Result2, T3 Result3)> WhenAll( + this (Task Task1, Task Task2, Task Task3) tasks) + { + var (task1, task2, task3) = tasks; + await Task.WhenAll(task1, task2, task3); + return (task1.Result, task2.Result, task3.Result); + } + + internal static async Task<(T1 Result1, T2 Result2, T3 Result3, T4 Result4)> WhenAll( + this (Task Task1, Task Task2, Task Task3, Task Task4) tasks) + { + var (task1, task2, task3, task4) = tasks; + await Task.WhenAll(task1, task2, task3, task4); + return (task1.Result, task2.Result, task3.Result, task4.Result); + } +} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Loans/LoanOverview.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Loans/LoanOverview.cs deleted file mode 100644 index 1583fed64..000000000 --- a/source/Gnomeshade.Avalonia.Core/Transactions/Loans/LoanOverview.cs +++ /dev/null @@ -1,48 +0,0 @@ -// 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 Gnomeshade.WebApi.Models.Transactions; - -namespace Gnomeshade.Avalonia.Core.Transactions.Loans; - -/// Overview of a single . -public sealed class LoanOverview : PropertyChangedBase -{ - /// Initializes a new instance of the class. - /// The id of the loan. - /// The name of the counterparty that issued the loan. - /// The name of the counterparty that received the loan. - /// The amount loaned. - /// The alphabetic code of the currency of . - public LoanOverview( - Guid id, - string issuingCounterparty, - string receivingCounterparty, - decimal amount, - string currency) - { - Id = id; - IssuingCounterparty = issuingCounterparty; - ReceivingCounterparty = receivingCounterparty; - Amount = amount; - Currency = currency; - } - - /// Gets the id of the loan. - public Guid Id { get; } - - /// Gets the name of the counterparty that issued the loan. - public string IssuingCounterparty { get; } - - /// Gets the name of the counterparty that received the loan. - public string ReceivingCounterparty { get; } - - /// Gets the amount loaned. - public decimal Amount { get; } - - /// Gets the alphabetic code of the currency of . - public string Currency { get; } -} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Loans/LoanPaymentUpsertionViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Loans/LoanPaymentUpsertionViewModel.cs new file mode 100644 index 000000000..d5822852b --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Transactions/Loans/LoanPaymentUpsertionViewModel.cs @@ -0,0 +1,152 @@ +// 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.ComponentModel; +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 PropertyChanged.SourceGenerator; + +namespace Gnomeshade.Avalonia.Core.Transactions.Loans; + +/// Creates or updates a single loan payment. +public sealed partial class LoanPaymentUpsertionViewModel : UpsertionViewModel +{ + private readonly Guid _transactionId; + + /// Gets all available loans. + [Notify(Setter.Private)] + private List _loans = []; + + /// Gets or sets the loan that this payment is a part of. + [Notify] + private Loan? _loan; + + /// Gets or sets the amount of the loan payment. + /// + [Notify] + private decimal? _amount; + + /// Gets or sets the interest amount of this loan payment. + [Notify] + private decimal? _interest; + + /// Initializes a new instance of the class. + /// Service for indicating the activity of the application to the user. + /// A strongly typed API client. + /// The id of the transaction to which to add the loan. + /// The id of the loan payment to edit. + public LoanPaymentUpsertionViewModel( + IActivityService activityService, + IGnomeshadeClient gnomeshadeClient, + Guid transactionId, + Guid? id) + : base(activityService, gnomeshadeClient) + { + _transactionId = transactionId; + Id = id; + + PropertyChanged += OnPropertyChanged; + } + + /// + public AutoCompleteSelector LoanSelector => AutoCompleteSelectors.Loan; + + /// + public override bool CanSave => + Loan is not null && + Amount is not null && + Interest is not null; + + /// + protected override async Task Refresh() + { + Loans = await GnomeshadeClient.GetLoansAsync(); + + if (Id is not { } id) + { + return; + } + + var payment = await GnomeshadeClient.GetLoanPaymentAsync(id); + Loan = Loans.Single(loan => loan.Id == payment.LoanId); + Amount = payment.Amount; + Interest = payment.Interest; + } + + /// + protected override async Task SaveValidatedAsync() + { + var creation = new LoanPaymentCreation + { + LoanId = Loan?.Id, + TransactionId = _transactionId, + Amount = Amount, + Interest = Interest, + }; + + var id = Id ?? Guid.NewGuid(); + await GnomeshadeClient.PutLoanPaymentAsync(id, creation); + return id; + } + + private async void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is not nameof(Loan) || Loan is not { } loan) + { + return; + } + + var accounts = await GnomeshadeClient.GetAccountsAsync(); + var transaction = await GnomeshadeClient.GetDetailedTransactionAsync(_transactionId); + + var sourceCounterparties = transaction + .Transfers + .Select(transfer => accounts + .Single(account => account.Currencies.Any(currency => currency.Id == transfer.SourceAccountId))) + .Select(account => account.CounterpartyId) + .Distinct() + .ToArray(); + + var targetCounterparties = transaction + .Transfers + .Select(transfer => accounts + .Single(account => account.Currencies.Any(currency => currency.Id == transfer.TargetAccountId))) + .Select(account => account.CounterpartyId) + .Distinct() + .ToArray(); + + if (sourceCounterparties is not [var source] || targetCounterparties is not [var target]) + { + return; + } + + if (loan.IssuingCounterpartyId == source && loan.ReceivingCounterpartyId == target) + { + Amount = transaction.Transfers.Sum(transfer => transfer.SourceAmount); + Interest = 0; + } + else if (loan.IssuingCounterpartyId == target && loan.ReceivingCounterpartyId == source) + { + if (transaction.Transfers.OrderByDescending(transfer => transfer.SourceAmount).ToArray() is [var amount, var interest]) + { + Amount = -amount.SourceAmount; + Interest = -interest.SourceAmount; + } + else if (transaction.Transfers is [var transfer]) + { + Amount = -transfer.SourceAmount; + Interest = 0; + } + } + } +} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Loans/LoanViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Loans/LoanPaymentViewModel.cs similarity index 69% rename from source/Gnomeshade.Avalonia.Core/Transactions/Loans/LoanViewModel.cs rename to source/Gnomeshade.Avalonia.Core/Transactions/Loans/LoanPaymentViewModel.cs index 119d3f133..b0ac45ac3 100644 --- a/source/Gnomeshade.Avalonia.Core/Transactions/Loans/LoanViewModel.cs +++ b/source/Gnomeshade.Avalonia.Core/Transactions/Loans/LoanPaymentViewModel.cs @@ -7,28 +7,29 @@ using System.Linq; using System.Threading.Tasks; +using Gnomeshade.Avalonia.Core.Loans; using Gnomeshade.WebApi.Client; using PropertyChanged.SourceGenerator; namespace Gnomeshade.Avalonia.Core.Transactions.Loans; -/// Overview of all loans for a single transaction. -public sealed partial class LoanViewModel : OverviewViewModel +/// Overview of all loan payments for a single transaction. +public sealed partial class LoanPaymentViewModel : OverviewViewModel { private readonly IGnomeshadeClient _gnomeshadeClient; private readonly Guid _transactionId; - private LoanUpsertionViewModel _details; + private LoanPaymentUpsertionViewModel _details; /// Gets the total loaned amount. [Notify(Setter.Private)] private decimal _total; - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// Service for indicating the activity of the application to the user. /// A strongly typed API client. /// The transaction for which to create a loan overview. - public LoanViewModel(IActivityService activityService, IGnomeshadeClient gnomeshadeClient, Guid transactionId) + public LoanPaymentViewModel(IActivityService activityService, IGnomeshadeClient gnomeshadeClient, Guid transactionId) : base(activityService) { _gnomeshadeClient = gnomeshadeClient; @@ -40,7 +41,7 @@ public LoanViewModel(IActivityService activityService, IGnomeshadeClient gnomesh } /// - public override LoanUpsertionViewModel Details + public override LoanPaymentUpsertionViewModel Details { get => _details; set @@ -62,15 +63,12 @@ public override async Task UpdateSelection() protected override async Task Refresh() { var transaction = await _gnomeshadeClient.GetDetailedTransactionAsync(_transactionId); - var counterparties = await _gnomeshadeClient.GetCounterpartiesAsync(); + var payments = await _gnomeshadeClient.GetLoanPaymentsForTransactionAsync(_transactionId); + var loans = await _gnomeshadeClient.GetLoansAsync(); var currencies = await _gnomeshadeClient.GetCurrenciesAsync(); - var overviews = transaction.Loans - .Select(loan => new LoanOverview( - loan.Id, - counterparties.Single(counterparty => counterparty.Id == loan.IssuingCounterpartyId).Name, - counterparties.Single(counterparty => counterparty.Id == loan.ReceivingCounterpartyId).Name, - loan.Amount, - currencies.Single(currency => currency.Id == loan.CurrencyId).AlphabeticCode)) + + var overviews = payments + .Select(payment => new LoanPaymentRow(payment, loans, currencies)) .ToList(); var selected = Selected; @@ -86,9 +84,10 @@ protected override async Task Refresh() } /// - protected override async Task DeleteAsync(LoanOverview row) + protected override async Task DeleteAsync(LoanPaymentRow row) { - await _gnomeshadeClient.DeleteLoanAsync(row.Id); + await _gnomeshadeClient.DeleteLoanPaymentAsync(row.Id); + await RefreshAsync(); } private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Loans/LoanUpsertionViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Loans/LoanUpsertionViewModel.cs deleted file mode 100644 index 23ac7558f..000000000 --- a/source/Gnomeshade.Avalonia.Core/Transactions/Loans/LoanUpsertionViewModel.cs +++ /dev/null @@ -1,123 +0,0 @@ -// 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.Transactions; - -using PropertyChanged.SourceGenerator; - -namespace Gnomeshade.Avalonia.Core.Transactions.Loans; - -/// Creates or updates a single loan. -public sealed partial class LoanUpsertionViewModel : UpsertionViewModel -{ - private readonly Guid _transactionId; - - /// Gets all available counterparties. - [Notify(Setter.Private)] - private List _counterparties; - - /// Gets all available currencies. - [Notify(Setter.Private)] - private List _currencies; - - /// Gets or sets the counterparty issuing the loan. - /// - /// - [Notify] - private Counterparty? _issuingCounterparty; - - /// Gets or sets the counterparty receiving the loan. - /// - /// - [Notify] - private Counterparty? _receivingCounterparty; - - /// Gets or sets the amount of the loan. - /// - [Notify] - private decimal? _amount; - - /// Gets or sets the currency of the loan. - /// - /// - [Notify] - private Currency? _currency; - - /// Initializes a new instance of the class. - /// Service for indicating the activity of the application to the user. - /// A strongly typed API client. - /// The id of the transaction to which to add the loan. - /// The id of the loan to edit. - public LoanUpsertionViewModel( - IActivityService activityService, - IGnomeshadeClient gnomeshadeClient, - Guid transactionId, - Guid? id) - : base(activityService, gnomeshadeClient) - { - _transactionId = transactionId; - Id = id; - - _counterparties = new(); - _currencies = new(); - } - - /// Gets a delegate for formatting a counterparty in an . - public AutoCompleteSelector CounterpartySelector => AutoCompleteSelectors.Counterparty; - - /// Gets a delegate for formatting a currency in an . - public AutoCompleteSelector CurrencySelector => AutoCompleteSelectors.Currency; - - /// - public override bool CanSave => - IssuingCounterparty is not null && - ReceivingCounterparty is not null && - IssuingCounterparty.Id != ReceivingCounterparty.Id && - Amount is not null && - Currency is not null; - - /// - protected override async Task Refresh() - { - Counterparties = await GnomeshadeClient.GetCounterpartiesAsync(); - Currencies = await GnomeshadeClient.GetCurrenciesAsync(); - - if (Id is null) - { - return; - } - - var loan = await GnomeshadeClient.GetTransactionLoanAsync(Id.Value); - IssuingCounterparty = Counterparties.Single(counterparty => counterparty.Id == loan.IssuingCounterpartyId); - ReceivingCounterparty = Counterparties.Single(counterparty => counterparty.Id == loan.ReceivingCounterpartyId); - Amount = loan.Amount; - Currency = Currencies.Single(currency => currency.Id == loan.CurrencyId); - } - - /// - protected override async Task SaveValidatedAsync() - { - var creation = new LoanCreation - { - TransactionId = _transactionId, - IssuingCounterpartyId = IssuingCounterparty?.Id, - ReceivingCounterpartyId = ReceivingCounterparty?.Id, - Amount = Amount, - CurrencyId = Currency?.Id, - }; - - var id = Id ?? Guid.NewGuid(); - await GnomeshadeClient.PutLoanAsync(id, creation); - return id; - } -} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Loans/Migration/LoanMigrationViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Loans/Migration/LoanMigrationViewModel.cs deleted file mode 100644 index 5b7a9041a..000000000 --- a/source/Gnomeshade.Avalonia.Core/Transactions/Loans/Migration/LoanMigrationViewModel.cs +++ /dev/null @@ -1,89 +0,0 @@ -// 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.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -using Gnomeshade.WebApi.Client; - -using NodaTime; - -using PropertyChanged.SourceGenerator; - -namespace Gnomeshade.Avalonia.Core.Transactions.Loans.Migration; - -/// Overview of loans to be migrated to named loans/loan payments. -public sealed partial class LoanMigrationViewModel : ViewModelBase -{ - private readonly IGnomeshadeClient _gnomeshadeClient; - - /// Gets a collection of all current loans. - [Notify(Setter.Private)] - private List _loans = []; - - /// Gets a collection of all loan payments that will be created. - [Notify(Setter.Private)] - private List _migratedLoans = []; - - /// Initializes a new instance of the class. - /// Service for indicating the activity of the application to the user. - /// A strongly typed API client. - public LoanMigrationViewModel(IActivityService activityService, IGnomeshadeClient gnomeshadeClient) - : base(activityService) - { - _gnomeshadeClient = gnomeshadeClient; - } - - /// - protected override async Task Refresh() - { - var loans = await _gnomeshadeClient.GetLoansAsync(); - var counterparties = await _gnomeshadeClient.GetCounterpartiesAsync(); - var transactions = await _gnomeshadeClient.GetDetailedTransactionsAsync(new(Instant.MinValue, Instant.MaxValue)); - var currencies = await _gnomeshadeClient.GetCurrenciesAsync(); - - MigratedLoans = loans - .OrderBy(loan => - { - var transaction = transactions.Single(transaction => transaction.Id == loan.TransactionId); - return transaction.ValuedAt ?? transaction.BookedAt; - }) - .GroupBy(loan => loan, new LoanCounterpartyComparer()) - .Select(grouping => - { - var counterparty1 = counterparties.Single(counterparty => counterparty.Id == grouping.Key.IssuingCounterpartyId); - var counterparty2 = counterparties.Single(counterparty => counterparty.Id == grouping.Key.ReceivingCounterpartyId); - return (counterparty1, counterparty2, grouping.ToArray()); - }) - .SelectMany(loanTuple => - { - var issuer = loanTuple.Item3.First().IssuingCounterpartyId == loanTuple.counterparty1.Id - ? loanTuple.counterparty1 - : loanTuple.counterparty2; - - var receiver = loanTuple.counterparty1 == issuer - ? loanTuple.counterparty2 - : loanTuple.counterparty1; - - return loanTuple.Item3.GroupBy(loan => loan.CurrencyId).Select(grouping => - { - var currency = currencies.Single(currency => currency.Id == grouping.Key); - var name = $"{issuer.Name} loan to {receiver.Name} ({currency.AlphabeticCode})"; - return new LoanMigrationRow(name, issuer.Name, receiver.Name, grouping.Select(loan => loan.Amount).ToList(), currency.AlphabeticCode); - }); - }) - .ToList(); - - Loans = transactions - .SelectMany(transaction => transaction.Loans) - .Select(loan => new LoanOverview( - loan.Id, - counterparties.Single(counterparty => counterparty.Id == loan.IssuingCounterpartyId).Name, - counterparties.Single(counterparty => counterparty.Id == loan.ReceivingCounterpartyId).Name, - loan.Amount, - currencies.Single(currency => currency.Id == loan.CurrencyId).AlphabeticCode)) - .ToList(); - } -} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/TransactionUpsertionViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionUpsertionViewModel.cs index 937a21a0b..08d7ebf0a 100644 --- a/source/Gnomeshade.Avalonia.Core/Transactions/TransactionUpsertionViewModel.cs +++ b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionUpsertionViewModel.cs @@ -44,7 +44,7 @@ public sealed partial class TransactionUpsertionViewModel : UpsertionViewModel /// Gets view model of all loans of this transaction. [Notify(Setter.Private)] - private LoanViewModel? _loans; + private LoanPaymentViewModel? _loans; /// Initializes a new instance of the class. /// Service for indicating the activity of the application to the user. diff --git a/source/Gnomeshade.Desktop/App.axaml b/source/Gnomeshade.Desktop/App.axaml index 0ff530fab..32b39e69b 100644 --- a/source/Gnomeshade.Desktop/App.axaml +++ b/source/Gnomeshade.Desktop/App.axaml @@ -6,6 +6,7 @@ xmlns:accounts="clr-namespace:Gnomeshade.WebApi.Models.Accounts;assembly=Gnomeshade.WebApi.Models" xmlns:products="clr-namespace:Gnomeshade.WebApi.Models.Products;assembly=Gnomeshade.WebApi.Models" xmlns:owners="clr-namespace:Gnomeshade.WebApi.Models.Owners;assembly=Gnomeshade.WebApi.Models" + xmlns:loans="clr-namespace:Gnomeshade.WebApi.Models.Loans;assembly=Gnomeshade.WebApi.Models" x:Class="Gnomeshade.Desktop.App" RequestedThemeVariant="Dark"> @@ -32,6 +33,10 @@ + + + + diff --git a/source/Gnomeshade.Desktop/Views/Loans/LoanUpsertionView.axaml b/source/Gnomeshade.Desktop/Views/Loans/LoanUpsertionView.axaml new file mode 100644 index 000000000..0e81cd4c1 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Loans/LoanUpsertionView.axaml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/Gnomeshade.Desktop/Views/Loans/LoanUpsertionView.axaml.cs b/source/Gnomeshade.Desktop/Views/Loans/LoanUpsertionView.axaml.cs new file mode 100644 index 000000000..386212388 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Loans/LoanUpsertionView.axaml.cs @@ -0,0 +1,17 @@ +// 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 Avalonia.Controls; + +using Gnomeshade.Avalonia.Core; +using Gnomeshade.Avalonia.Core.Loans; + +namespace Gnomeshade.Desktop.Views.Loans; + +/// +public sealed partial class LoanUpsertionView : UserControl, IView +{ + /// Initializes a new instance of the class. + public LoanUpsertionView() => InitializeComponent(); +} diff --git a/source/Gnomeshade.Desktop/Views/Loans/LoanView.axaml b/source/Gnomeshade.Desktop/Views/Loans/LoanView.axaml new file mode 100644 index 000000000..9e73becd0 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Loans/LoanView.axaml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/Gnomeshade.Desktop/Views/Loans/LoanView.axaml.cs b/source/Gnomeshade.Desktop/Views/Loans/LoanView.axaml.cs new file mode 100644 index 000000000..dd8849768 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Loans/LoanView.axaml.cs @@ -0,0 +1,17 @@ +// 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 Avalonia.Controls; + +using Gnomeshade.Avalonia.Core; +using Gnomeshade.Avalonia.Core.Loans; + +namespace Gnomeshade.Desktop.Views.Loans; + +/// +public sealed partial class LoanView : UserControl, IView +{ + /// Initializes a new instance of the class. + public LoanView() => InitializeComponent(); +} diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Loans/Migration/LoanMigrationView.axaml b/source/Gnomeshade.Desktop/Views/Loans/Migration/LoanMigrationView.axaml similarity index 76% rename from source/Gnomeshade.Desktop/Views/Transactions/Loans/Migration/LoanMigrationView.axaml rename to source/Gnomeshade.Desktop/Views/Loans/Migration/LoanMigrationView.axaml index 323263fd8..c1095cb26 100644 --- a/source/Gnomeshade.Desktop/Views/Transactions/Loans/Migration/LoanMigrationView.axaml +++ b/source/Gnomeshade.Desktop/Views/Loans/Migration/LoanMigrationView.axaml @@ -4,12 +4,11 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:dd="clr-namespace:Gnomeshade.Avalonia.Core.DesignTime;assembly=Gnomeshade.Avalonia.Core" - xmlns:migration="clr-namespace:Gnomeshade.Avalonia.Core.Transactions.Loans.Migration;assembly=Gnomeshade.Avalonia.Core" - xmlns:loans="clr-namespace:Gnomeshade.Avalonia.Core.Transactions.Loans;assembly=Gnomeshade.Avalonia.Core" xmlns:system="clr-namespace:System;assembly=System.Runtime" + xmlns:migration="clr-namespace:Gnomeshade.Avalonia.Core.Loans.Migration;assembly=Gnomeshade.Avalonia.Core" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" d:DataContext="{x:Static dd:DesignTimeData.LoanMigrationViewModel}" - x:Class="Gnomeshade.Desktop.Views.Transactions.Loans.Migration.LoanMigrationView" + x:Class="Gnomeshade.Desktop.Views.Loans.Migration.LoanMigrationView" x:DataType="migration:LoanMigrationViewModel"> @@ -23,19 +22,19 @@ CanUserReorderColumns="True" CanUserResizeColumns="True" CanUserSortColumns="True"> + Binding="{Binding Issuer, Mode=OneWay}" /> + Binding="{Binding Receiver, Mode=OneWay}" /> @@ -77,6 +76,13 @@ Text="{Binding Currency, Mode=OneWay}" /> + + + + + @@ -88,13 +94,24 @@ - - + + + + + diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Loans/Migration/LoanMigrationView.axaml.cs b/source/Gnomeshade.Desktop/Views/Loans/Migration/LoanMigrationView.axaml.cs similarity index 81% rename from source/Gnomeshade.Desktop/Views/Transactions/Loans/Migration/LoanMigrationView.axaml.cs rename to source/Gnomeshade.Desktop/Views/Loans/Migration/LoanMigrationView.axaml.cs index 6e7c71781..3a093b545 100644 --- a/source/Gnomeshade.Desktop/Views/Transactions/Loans/Migration/LoanMigrationView.axaml.cs +++ b/source/Gnomeshade.Desktop/Views/Loans/Migration/LoanMigrationView.axaml.cs @@ -5,9 +5,9 @@ using Avalonia.Controls; using Gnomeshade.Avalonia.Core; -using Gnomeshade.Avalonia.Core.Transactions.Loans.Migration; +using Gnomeshade.Avalonia.Core.Loans.Migration; -namespace Gnomeshade.Desktop.Views.Transactions.Loans.Migration; +namespace Gnomeshade.Desktop.Views.Loans.Migration; /// public sealed partial class LoanMigrationView : UserControl, IView diff --git a/source/Gnomeshade.Desktop/Views/MainWindow.axaml b/source/Gnomeshade.Desktop/Views/MainWindow.axaml index a4939c38c..4bad0de7a 100644 --- a/source/Gnomeshade.Desktop/Views/MainWindow.axaml +++ b/source/Gnomeshade.Desktop/Views/MainWindow.axaml @@ -68,6 +68,9 @@ + diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanUpsertionView.axaml b/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanUpsertionView.axaml index 260542aeb..bd2c69ebe 100644 --- a/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanUpsertionView.axaml +++ b/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanUpsertionView.axaml @@ -5,38 +5,24 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:design="clr-namespace:Gnomeshade.Avalonia.Core.DesignTime;assembly=Gnomeshade.Avalonia.Core" xmlns:loans="clr-namespace:Gnomeshade.Avalonia.Core.Transactions.Loans;assembly=Gnomeshade.Avalonia.Core" - mc:Ignorable="d" d:DataContext="{x:Static design:DesignTimeData.LoanUpsertionViewModel}" + mc:Ignorable="d" d:DataContext="{x:Static design:DesignTimeData.LoanPaymentUpsertionViewModel}" x:Class="Gnomeshade.Desktop.Views.Transactions.Loans.LoanUpsertionView" - x:DataType="loans:LoanUpsertionViewModel"> + x:DataType="loans:LoanPaymentUpsertionViewModel"> - - + ItemsSource="{Binding Loans}" + ItemSelector="{Binding LoanSelector}" + SelectedItem="{Binding Loan, Mode=TwoWay}" /> - + diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanUpsertionView.axaml.cs b/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanUpsertionView.axaml.cs index 0252ae9d6..7be88ad6b 100644 --- a/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanUpsertionView.axaml.cs +++ b/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanUpsertionView.axaml.cs @@ -10,8 +10,8 @@ namespace Gnomeshade.Desktop.Views.Transactions.Loans; -/// -public sealed partial class LoanUpsertionView : UserControl, IView +/// +public sealed partial class LoanUpsertionView : UserControl, IView { /// Initializes a new instance of the class. public LoanUpsertionView() diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanView.axaml b/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanView.axaml index f3e57a187..137e2eb47 100644 --- a/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanView.axaml +++ b/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanView.axaml @@ -4,12 +4,13 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:design="clr-namespace:Gnomeshade.Avalonia.Core.DesignTime;assembly=Gnomeshade.Avalonia.Core" - xmlns:loans="clr-namespace:Gnomeshade.Avalonia.Core.Transactions.Loans;assembly=Gnomeshade.Avalonia.Core" xmlns:core="clr-namespace:Gnomeshade.Avalonia.Core;assembly=Gnomeshade.Avalonia.Core" + xmlns:loans="clr-namespace:Gnomeshade.Avalonia.Core.Loans;assembly=Gnomeshade.Avalonia.Core" + xmlns:transactionLoans="clr-namespace:Gnomeshade.Avalonia.Core.Transactions.Loans;assembly=Gnomeshade.Avalonia.Core" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - d:DataContext="{x:Static design:DesignTimeData.LoanViewModel}" + d:DataContext="{x:Static design:DesignTimeData.LoanPaymentViewModel}" x:Class="Gnomeshade.Desktop.Views.Transactions.Loans.LoanView" - x:DataType="loans:LoanViewModel"> + x:DataType="transactionLoans:LoanPaymentViewModel"> + x:DataType="loans:LoanPaymentRow" + Header="Loan" IsReadOnly="True" + Binding="{Binding Loan, Mode=OneWay}" /> - + diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanView.axaml.cs b/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanView.axaml.cs index caece1e18..98ae176f1 100644 --- a/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanView.axaml.cs +++ b/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanView.axaml.cs @@ -10,8 +10,8 @@ namespace Gnomeshade.Desktop.Views.Transactions.Loans; -/// -public sealed partial class LoanView : UserControl, IView +/// +public sealed partial class LoanView : UserControl, IView { /// Initializes a new instance of the class. public LoanView()