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:
Andrew Camilleri 2021-06-30 08:59:01 +01:00 committed by GitHub
parent 33de4cccfc
commit 6c856aba48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 267 additions and 270 deletions

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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

View File

@ -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();
}
}

View File

@ -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>