mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 21:32:27 +01:00
Introduce Server paging for Payouts List (#2564)
* Introduce Server paging for Payouts List * Add paging params * Minor code and formatting improvements * View updates * Apply suggestions from code review Co-authored-by: Zaxounette <51208677+Zaxounette@users.noreply.github.com> * fix tests Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: Zaxounette <51208677+Zaxounette@users.noreply.github.com>
This commit is contained in:
parent
33de4cccfc
commit
6c856aba48
@ -912,113 +912,113 @@ namespace BTCPayServer.Tests
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanUsePullPaymentsViaUI()
|
||||
{
|
||||
using (var s = SeleniumTester.Create())
|
||||
{
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.GenerateWallet("BTC", "", true, true);
|
||||
using var s = SeleniumTester.Create();
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.GenerateWallet("BTC", "", true, true);
|
||||
|
||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||
await s.FundStoreWallet(denomination: 50.0m);
|
||||
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
|
||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");;
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||
await s.FundStoreWallet(denomination: 50.0m);
|
||||
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
|
||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");;
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
|
||||
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
|
||||
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
|
||||
|
||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP2");
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("100.0");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP2");
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("100.0");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
|
||||
// This should select the first View, ie, the last one PP2
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
var address = await s.Server.ExplorerNode.GetNewAddressAsync();
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("15" + Keys.Enter);
|
||||
s.FindAlertMessage();
|
||||
// This should select the first View, ie, the last one PP2
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
var address = await s.Server.ExplorerNode.GetNewAddressAsync();
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("15" + Keys.Enter);
|
||||
s.FindAlertMessage();
|
||||
|
||||
// We should not be able to use an address already used
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter);
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
|
||||
// We should not be able to use an address already used
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter);
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
|
||||
|
||||
address = await s.Server.ExplorerNode.GetNewAddressAsync();
|
||||
s.Driver.FindElement(By.Id("Destination")).Clear();
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter);
|
||||
s.FindAlertMessage();
|
||||
Assert.Contains("Awaiting Approval", s.Driver.PageSource);
|
||||
address = await s.Server.ExplorerNode.GetNewAddressAsync();
|
||||
s.Driver.FindElement(By.Id("Destination")).Clear();
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter);
|
||||
s.FindAlertMessage();
|
||||
Assert.Contains("Awaiting Approval", s.Driver.PageSource);
|
||||
|
||||
var viewPullPaymentUrl = s.Driver.Url;
|
||||
// This one should have nothing
|
||||
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
|
||||
var payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
|
||||
Assert.Equal(2, payouts.Count);
|
||||
payouts[1].Click();
|
||||
Assert.Empty(s.Driver.FindElements(By.ClassName("payout")));
|
||||
// PP2 should have payouts
|
||||
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
|
||||
payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
|
||||
payouts[0].Click();
|
||||
var viewPullPaymentUrl = s.Driver.Url;
|
||||
// This one should have nothing
|
||||
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
|
||||
var payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
|
||||
Assert.Equal(2, payouts.Count);
|
||||
payouts[1].Click();
|
||||
Assert.Empty(s.Driver.FindElements(By.ClassName("payout")));
|
||||
// PP2 should have payouts
|
||||
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
|
||||
payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
|
||||
payouts[0].Click();
|
||||
|
||||
Assert.NotEmpty(s.Driver.FindElements(By.ClassName("payout")));
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
|
||||
Assert.NotEmpty(s.Driver.FindElements(By.ClassName("payout")));
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
|
||||
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
|
||||
s.FindAlertMessage();
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
|
||||
s.FindAlertMessage();
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.Contains("badge transactionLabel", s.Driver.PageSource);
|
||||
});
|
||||
Assert.Equal("payout", s.Driver.FindElement(By.ClassName("transactionLabel")).Text);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.Contains("badge transactionLabel", s.Driver.PageSource);
|
||||
});
|
||||
Assert.Equal("payout", s.Driver.FindElement(By.ClassName("transactionLabel")).Text);
|
||||
|
||||
s.GoToWallet(navPages: WalletsNavPages.Payouts);
|
||||
ReadOnlyCollection<IWebElement> txs;
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
s.GoToWallet(navPages: WalletsNavPages.Payouts);
|
||||
var x = s.Driver.PageSource;
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.InProgress}-view")).Click();
|
||||
ReadOnlyCollection<IWebElement> txs;
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
|
||||
txs = s.Driver.FindElements(By.ClassName("transaction-link"));
|
||||
Assert.Equal(2, txs.Count);
|
||||
});
|
||||
|
||||
s.Driver.Navigate().GoToUrl(viewPullPaymentUrl);
|
||||
txs = s.Driver.FindElements(By.ClassName("transaction-link"));
|
||||
Assert.Equal(2, txs.Count);
|
||||
Assert.Contains("In Progress", s.Driver.PageSource);
|
||||
});
|
||||
|
||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||
s.Driver.Navigate().GoToUrl(viewPullPaymentUrl);
|
||||
txs = s.Driver.FindElements(By.ClassName("transaction-link"));
|
||||
Assert.Equal(2, txs.Count);
|
||||
Assert.Contains(PayoutState.InProgress.GetStateString(), s.Driver.PageSource);
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.Contains("Completed", s.Driver.PageSource);
|
||||
});
|
||||
await s.Server.ExplorerNode.GenerateAsync(10);
|
||||
var pullPaymentId = viewPullPaymentUrl.Split('/').Last();
|
||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
using var ctx = s.Server.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
|
||||
var payoutsData = await ctx.Payouts.Where(p => p.PullPaymentDataId == pullPaymentId).ToListAsync();
|
||||
Assert.True(payoutsData.All(p => p.State == PayoutState.Completed));
|
||||
});
|
||||
}
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.Contains(PayoutState.Completed.GetStateString(), s.Driver.PageSource);
|
||||
});
|
||||
await s.Server.ExplorerNode.GenerateAsync(10);
|
||||
var pullPaymentId = viewPullPaymentUrl.Split('/').Last();
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
using var ctx = s.Server.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
|
||||
var payoutsData = await ctx.Payouts.Where(p => p.PullPaymentDataId == pullPaymentId).ToListAsync();
|
||||
Assert.True(payoutsData.All(p => p.State == PayoutState.Completed));
|
||||
});
|
||||
}
|
||||
|
||||
private static void CanBrowseContent(SeleniumTester s)
|
||||
|
@ -24,8 +24,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class WalletsController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("{walletId}/pull-payments/new")]
|
||||
[HttpGet("{walletId}/pull-payments/new")]
|
||||
public IActionResult NewPullPayment([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
{
|
||||
@ -40,9 +39,8 @@ namespace BTCPayServer.Controllers
|
||||
EmbeddedCSS = "",
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/pull-payments/new")]
|
||||
|
||||
[HttpPost("{walletId}/pull-payments/new")]
|
||||
public async Task<IActionResult> NewPullPayment([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, NewPullPaymentModel model)
|
||||
{
|
||||
@ -86,9 +84,8 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
return RedirectToAction(nameof(PullPayments), new { walletId = walletId.ToString() });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/pull-payments")]
|
||||
|
||||
[HttpGet("{walletId}/pull-payments")]
|
||||
public async Task<IActionResult> PullPayments(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
@ -149,8 +146,7 @@ namespace BTCPayServer.Controllers
|
||||
return time;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/pull-payments/{pullPaymentId}/archive")]
|
||||
[HttpGet("{walletId}/pull-payments/{pullPaymentId}/archive")]
|
||||
public IActionResult ArchivePullPayment(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId,
|
||||
@ -164,8 +160,8 @@ namespace BTCPayServer.Controllers
|
||||
Action = "Archive"
|
||||
});
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{walletId}/pull-payments/{pullPaymentId}/archive")]
|
||||
|
||||
[HttpPost("{walletId}/pull-payments/{pullPaymentId}/archive")]
|
||||
public async Task<IActionResult> ArchivePullPaymentPost(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId,
|
||||
@ -180,8 +176,7 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(PullPayments), new { walletId = walletId.ToString() });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/payouts")]
|
||||
[HttpPost("{walletId}/payouts")]
|
||||
public async Task<IActionResult> PayoutsPost(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, PayoutsModel vm, CancellationToken cancellationToken)
|
||||
@ -312,7 +307,7 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
if (result != PayoutPaidRequest.PayoutPaidResult.Ok)
|
||||
{
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = PayoutPaidRequest.GetErrorMessage(result),
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
@ -362,65 +357,77 @@ namespace BTCPayServer.Controllers
|
||||
return payouts;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/payouts")]
|
||||
[HttpGet("{walletId}/payouts")]
|
||||
public async Task<IActionResult> Payouts(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, PayoutsModel vm = null)
|
||||
WalletId walletId, string pullPaymentId, PayoutState payoutState,
|
||||
int skip = 0, int count = 50)
|
||||
{
|
||||
vm ??= new PayoutsModel();
|
||||
vm.PayoutStateSets ??= ((PayoutState[]) Enum.GetValues(typeof(PayoutState))).Select(state =>
|
||||
new PayoutsModel.PayoutStateSet() {State = state, Payouts = new List<PayoutsModel.PayoutModel>()}).ToList();
|
||||
using var ctx = this._dbContextFactory.CreateContext();
|
||||
var vm = this.ParseListQuery(new PayoutsModel
|
||||
{
|
||||
PaymentMethodId = new PaymentMethodId(walletId.CryptoCode, PaymentTypes.BTCLike),
|
||||
PullPaymentId = pullPaymentId,
|
||||
PayoutState = payoutState,
|
||||
Skip = skip,
|
||||
Count = count
|
||||
});
|
||||
vm.Payouts = new List<PayoutsModel.PayoutModel>();
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var storeId = walletId.StoreId;
|
||||
vm.PaymentMethodId = new PaymentMethodId(walletId.CryptoCode, PaymentTypes.BTCLike);
|
||||
var payoutRequest = ctx.Payouts.Where(p => p.PullPaymentData.StoreId == storeId && !p.PullPaymentData.Archived);
|
||||
if (vm.PullPaymentId != null)
|
||||
{
|
||||
payoutRequest = payoutRequest.Where(p => p.PullPaymentDataId == vm.PullPaymentId);
|
||||
vm.PullPaymentName = (await ctx.PullPayments.FindAsync(pullPaymentId)).GetBlob().Name;
|
||||
}
|
||||
if (vm.PaymentMethodId != null)
|
||||
{
|
||||
var pmiStr = vm.PaymentMethodId.ToString();
|
||||
payoutRequest = payoutRequest.Where(p => p.PaymentMethodId == pmiStr);
|
||||
}
|
||||
|
||||
vm.PayoutStateCount = payoutRequest.GroupBy(data => data.State)
|
||||
.Select(e => new {e.Key, Count = e.Count()})
|
||||
.ToDictionary(arg => arg.Key, arg => arg.Count);
|
||||
foreach (PayoutState value in Enum.GetValues(typeof(PayoutState)))
|
||||
{
|
||||
if(vm.PayoutStateCount.ContainsKey(value))
|
||||
continue;
|
||||
vm.PayoutStateCount.Add(value, 0);
|
||||
}
|
||||
|
||||
vm.PayoutStateCount = vm.PayoutStateCount.OrderBy(pair => pair.Key)
|
||||
.ToDictionary(pair => pair.Key, pair => pair.Value);
|
||||
|
||||
payoutRequest = payoutRequest.Where(p => p.State == vm.PayoutState);
|
||||
vm.Total = await payoutRequest.CountAsync();
|
||||
payoutRequest = payoutRequest.Skip(vm.Skip).Take(vm.Count);
|
||||
|
||||
var payouts = await payoutRequest.OrderByDescending(p => p.Date)
|
||||
.Select(o => new
|
||||
{
|
||||
Payout = o,
|
||||
PullPayment = o.PullPaymentData
|
||||
}).ToListAsync();
|
||||
foreach (var stateSet in payouts.GroupBy(arg => arg.Payout.State))
|
||||
foreach (var item in payouts)
|
||||
{
|
||||
var state = vm.PayoutStateSets.SingleOrDefault(set => set.State == stateSet.Key);
|
||||
if (state == null)
|
||||
var ppBlob = item.PullPayment.GetBlob();
|
||||
var payoutBlob = item.Payout.GetBlob(_jsonSerializerSettings);
|
||||
var m = new PayoutsModel.PayoutModel
|
||||
{
|
||||
state = new PayoutsModel.PayoutStateSet()
|
||||
{
|
||||
Payouts = new List<PayoutsModel.PayoutModel>(), State = stateSet.Key
|
||||
};
|
||||
vm.PayoutStateSets.Add(state);
|
||||
}
|
||||
|
||||
foreach (var item in stateSet)
|
||||
{
|
||||
|
||||
if (item.Payout.GetPaymentMethodId() != vm.PaymentMethodId)
|
||||
continue;
|
||||
var ppBlob = item.PullPayment.GetBlob();
|
||||
var payoutBlob = item.Payout.GetBlob(_jsonSerializerSettings);
|
||||
var m = new PayoutsModel.PayoutModel();
|
||||
m.PullPaymentId = item.PullPayment.Id;
|
||||
m.PullPaymentName = ppBlob.Name ?? item.PullPayment.Id;
|
||||
m.Date = item.Payout.Date;
|
||||
m.PayoutId = item.Payout.Id;
|
||||
m.Amount = _currencyTable.DisplayFormatCurrency(payoutBlob.Amount, ppBlob.Currency);
|
||||
m.Destination = payoutBlob.Destination;
|
||||
var handler = _payoutHandlers
|
||||
.FirstOrDefault(handler => handler.CanHandle(item.Payout.GetPaymentMethodId()));
|
||||
var proofBlob = handler?.ParseProof(item.Payout);
|
||||
m.ProofLink = proofBlob?.Link;
|
||||
state.Payouts.Add(m);
|
||||
|
||||
}
|
||||
PullPaymentId = item.PullPayment.Id,
|
||||
PullPaymentName = ppBlob.Name ?? item.PullPayment.Id,
|
||||
Date = item.Payout.Date,
|
||||
PayoutId = item.Payout.Id,
|
||||
Amount = _currencyTable.DisplayFormatCurrency(payoutBlob.Amount, ppBlob.Currency),
|
||||
Destination = payoutBlob.Destination
|
||||
};
|
||||
var handler = _payoutHandlers
|
||||
.FirstOrDefault(handler => handler.CanHandle(item.Payout.GetPaymentMethodId()));
|
||||
var proofBlob = handler?.ParseProof(item.Payout);
|
||||
m.ProofLink = proofBlob?.Link;
|
||||
vm.Payouts.Add(m);
|
||||
}
|
||||
|
||||
vm.PayoutStateSets = vm.PayoutStateSets.Where(set => set.Payouts?.Any() is true).ToList();
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@ -21,6 +22,8 @@ namespace BTCPayServer
|
||||
prop = typeof(UserPrefsCookie).GetProperty(nameof(UserPrefsCookie.PaymentRequestsQuery));
|
||||
else if (model is UsersViewModel)
|
||||
prop = typeof(UserPrefsCookie).GetProperty(nameof(UserPrefsCookie.UsersQuery));
|
||||
else if (model is PayoutsModel)
|
||||
prop = typeof(UserPrefsCookie).GetProperty(nameof(UserPrefsCookie.UsersQuery));
|
||||
else
|
||||
throw new Exception("Unsupported BasePagingViewModel for cookie user preferences saving");
|
||||
|
||||
@ -78,6 +81,7 @@ namespace BTCPayServer
|
||||
|
||||
public ListQueryDataHolder PaymentRequestsQuery { get; set; }
|
||||
public ListQueryDataHolder UsersQuery { get; set; }
|
||||
public ListQueryDataHolder PayoutsQuery { get; set; }
|
||||
}
|
||||
|
||||
class ListQueryDataHolder
|
||||
|
@ -2,18 +2,21 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class PayoutsModel
|
||||
public class PayoutsModel : BasePagingViewModel
|
||||
{
|
||||
public string PullPaymentId { get; set; }
|
||||
public string Command { get; set; }
|
||||
public List<PayoutStateSet> PayoutStateSets{ get; set; }
|
||||
public Dictionary<PayoutState, int> PayoutStateCount { get; set; }
|
||||
public PaymentMethodId PaymentMethodId { get; set; }
|
||||
|
||||
public List<PayoutModel> Payouts { get; set; }
|
||||
public PayoutState PayoutState { get; set; }
|
||||
public string PullPaymentName { get; set; }
|
||||
|
||||
public class PayoutModel
|
||||
{
|
||||
public string PayoutId { get; set; }
|
||||
@ -26,16 +29,9 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public string ProofLink { get; set; }
|
||||
}
|
||||
|
||||
public class PayoutStateSet
|
||||
{
|
||||
public PayoutState State { get; set; }
|
||||
public List<PayoutModel> Payouts { get; set; }
|
||||
}
|
||||
|
||||
public string[] GetSelectedPayouts(PayoutState state)
|
||||
{
|
||||
return PayoutStateSets.Where(set => set.State == state)
|
||||
.SelectMany(set => set.Payouts.Where(model => model.Selected).Select(model => model.PayoutId))
|
||||
return Payouts.Where(model => model.Selected).Select(model => model.PayoutId)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,22 @@
|
||||
@inject IEnumerable<IPayoutHandler> PayoutHandlers;
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePageAndTitle(WalletsNavPages.Payouts, $"Manage {Model.PaymentMethodId.ToPrettyString()} payouts", Context.GetStoreData().StoreName);
|
||||
ViewData.SetActivePageAndTitle(WalletsNavPages.Payouts, $"Manage {Model.PaymentMethodId.ToPrettyString()} payouts{(string.IsNullOrEmpty(Model.PullPaymentName) ? string.Empty : " for pull payment " + Model.PullPaymentName)}", Context.GetStoreData().StoreName);
|
||||
|
||||
var stateActions = new List<(string Action, string Text)>();
|
||||
switch (Model.PayoutState)
|
||||
{
|
||||
case PayoutState.AwaitingApproval:
|
||||
stateActions.Add(("approve", "Approve selected payouts"));
|
||||
stateActions.Add(("approve-pay", "Approve & Send selected payouts"));
|
||||
stateActions.Add(("cancel", "Cancel selected payouts"));
|
||||
break;
|
||||
case PayoutState.AwaitingPayment:
|
||||
stateActions.Add(("pay", "Send selected payouts"));
|
||||
stateActions.Add(("cancel", "Cancel selected payouts"));
|
||||
stateActions.Add(("mark-paid", "Mark selected payouts as already paid"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@section PageFootContent {
|
||||
@ -20,127 +35,102 @@
|
||||
}
|
||||
|
||||
<form method="post">
|
||||
<h4 class="mb-3">@ViewData["Title"]</h4>
|
||||
@if (!Model.PayoutStateSets.Any())
|
||||
{
|
||||
<p class="text-secondary mt-3">
|
||||
There are no payouts yet.
|
||||
</p>
|
||||
}
|
||||
<input type="hidden" asp-for="PaymentMethodId"/>
|
||||
<input type="hidden" asp-for="PayoutState"/>
|
||||
<h4 class="mb-4">@ViewData["Title"]</h4>
|
||||
|
||||
<div class="row">
|
||||
<ul class="nav nav-pills">
|
||||
@for (var index = 0; index < Model.PayoutStateSets.Count; index++)
|
||||
{
|
||||
var state = Model.PayoutStateSets[index];
|
||||
<li class="nav-item py-0">
|
||||
<a class="nav-link me-1 @(index == 0 ? "active" : "")" data-bs-toggle="tab" href="#@state.State" role="tab">@state.State.GetStateString() (@state.Payouts.Count)</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<div class="col-auto">
|
||||
<ul class="nav nav-pills">
|
||||
@foreach (var state in Model.PayoutStateCount)
|
||||
{
|
||||
<li class="nav-item py-0">
|
||||
<a id="@state.Key-view" asp-action="Payouts" asp-route-walletId="@Context.GetRouteValue("walletId")" asp-route-payoutState="@state.Key" asp-route-pullPaymentId="@Model.PullPaymentId" class="nav-link me-1 @(state.Key == Model.PayoutState ? "active" : "")" role="tab">@state.Key.GetStateString() (@state.Value)</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@if (Model.Payouts.Any() && stateActions.Any())
|
||||
{
|
||||
<div class="dropdown col-auto mt-4 ms-xl-auto mt-xl-0">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" id="@Model.PayoutState-actions">
|
||||
Actions
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="@Model.PayoutState-actions">
|
||||
@foreach (var action in stateActions)
|
||||
{
|
||||
<button type="submit" id="@Model.PayoutState-@action.Action" name="Command" class="dropdown-item" role="button" value="@Model.PayoutState-@action.Action">@action.Text</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="tab-content w-100">
|
||||
@for (var index = 0; index < Model.PayoutStateSets.Count; index++)
|
||||
{
|
||||
var state = Model.PayoutStateSets[index];
|
||||
var stateActions = new List<(string Action, string Text)>();
|
||||
switch (state.State)
|
||||
{
|
||||
case PayoutState.AwaitingApproval:
|
||||
stateActions.Add(("approve", "Approve selected payouts"));
|
||||
stateActions.Add(("approve-pay", "Approve & Send selected payouts"));
|
||||
stateActions.Add(("cancel", "Cancel selected payouts"));
|
||||
break;
|
||||
case PayoutState.AwaitingPayment:
|
||||
stateActions.Add(("pay", "Send selected payouts"));
|
||||
stateActions.Add(("cancel", "Cancel selected payouts"));
|
||||
stateActions.Add(("mark-paid", "Mark selected payouts as already paid"));
|
||||
break;
|
||||
}
|
||||
<div class="tab-pane @(index == 0 ? "active" : "") " id="@state.State" role="tabpanel">
|
||||
<input type="hidden" asp-for="PayoutStateSets[index].State"/>
|
||||
<input type="hidden" asp-for="PaymentMethodId"/>
|
||||
|
||||
@if (state.Payouts.Any() && stateActions.Any())
|
||||
<div>
|
||||
@if (Model.Payouts.Any())
|
||||
{
|
||||
<table class="table table-sm table-responsive-lg">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>
|
||||
<input id="@Model.PayoutState-selectAllCheckbox" type="checkbox" class="form-check-input" onclick="selectAll(this, '@Model.PayoutState.ToString()'); return true;"/>
|
||||
</th>
|
||||
<th style="min-width: 90px;" class="col-md-auto">
|
||||
Date
|
||||
</th>
|
||||
<th class="text-start">Source</th>
|
||||
<th class="text-start">Destination</th>
|
||||
<th class="text-end">Amount</th>
|
||||
@if (Model.PayoutState != PayoutState.AwaitingApproval)
|
||||
{
|
||||
<th class="text-end">Transaction</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (int i = 0; i < Model.Payouts.Count; i++)
|
||||
{
|
||||
<div class="dropdown mt-2">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" id="@state.State-actions">
|
||||
Actions
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="@state.State-actions">
|
||||
@foreach (var action in stateActions)
|
||||
{
|
||||
<button type="submit" id="@state.State-@action.Action" name="Command" class="dropdown-item" role="button" value="@state.State-@action.Action">@action.Text</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
@if (state.Payouts.Any())
|
||||
{
|
||||
<table class="table table-sm table-responsive-lg">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>
|
||||
<input id="@state.State-selectAllCheckbox" type="checkbox" class="form-check-input" onclick="selectAll(this, '@state.State.ToString()'); return true;"/>
|
||||
</th>
|
||||
<th style="min-width: 90px;" class="col-md-auto">
|
||||
Date
|
||||
</th>
|
||||
<th class="text-start">Source</th>
|
||||
<th class="text-start">Destination</th>
|
||||
<th class="text-end">Amount</th>
|
||||
@if (state.State != PayoutState.AwaitingApproval)
|
||||
var pp = Model.Payouts[i];
|
||||
<tr class="payout">
|
||||
<td>
|
||||
<span>
|
||||
<input type="checkbox" class="selection-item-@Model.PayoutState.ToString() form-check-input" asp-for="Payouts[i].Selected"/>
|
||||
<input type="hidden" asp-for="Payouts[i].PayoutId"/>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>@pp.Date.ToBrowserDate()</span>
|
||||
</td>
|
||||
<td class="mw-100">
|
||||
<span>@pp.PullPaymentName</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>@pp.Destination</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span>@pp.Amount</span>
|
||||
</td>
|
||||
@if (Model.PayoutState != PayoutState.AwaitingApproval)
|
||||
{
|
||||
<td class="text-end">
|
||||
@if (!(pp.ProofLink is null))
|
||||
{
|
||||
<th class="text-end">Transaction</th>
|
||||
<a class="transaction-link" href="@pp.ProofLink">Link</a>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (int i = 0; i < state.Payouts.Count; i++)
|
||||
{
|
||||
var pp = state.Payouts[i];
|
||||
<tr class="payout">
|
||||
<td>
|
||||
<span>
|
||||
<input type="checkbox" class="selection-item-@state.State.ToString() form-check-input" asp-for="PayoutStateSets[index].Payouts[i].Selected"/>
|
||||
<input type="hidden" asp-for="PayoutStateSets[index].Payouts[i].PayoutId"/>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>@pp.Date.ToBrowserDate()</span>
|
||||
</td>
|
||||
<td class="mw-100">
|
||||
<span>@pp.PullPaymentName</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>@pp.Destination</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span>@pp.Amount</span>
|
||||
</td>
|
||||
@if (state.State != PayoutState.AwaitingApproval)
|
||||
{
|
||||
<td class="text-end">
|
||||
@if (!(pp.ProofLink is null))
|
||||
{
|
||||
<a class="transaction-link" href="@pp.ProofLink">Link</a>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="mb-0 p-4" id="@state.State-no-payouts">No payouts.</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="mb-0 py-4" id="@Model.PayoutState-no-payouts">There are no payouts matching this criteria.</p>
|
||||
}
|
||||
</div>
|
||||
<vc:pager view-model="Model"/>
|
||||
</div>
|
||||
</form>
|
||||
|
Loading…
Reference in New Issue
Block a user