mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Improve invoice filtering UI (#4914)
* Improve invoice filtering UI Closes #3664. * UI updates * Add app filter * Add indicator for active filters * updates text * Improve selected filter display * Apply suggestions from code review --------- Co-authored-by: dstrukt <gfxdsign@gmail.com>
This commit is contained in:
parent
3d57b944ca
commit
6c6544bf9b
10 changed files with 347 additions and 125 deletions
|
@ -1042,14 +1042,13 @@ namespace BTCPayServer.Tests
|
|||
[Fact]
|
||||
public void CanParseFilter()
|
||||
{
|
||||
var storeId = "6DehZnc9S7qC6TUTNWuzJ1pFsHTHvES6An21r3MjvLey";
|
||||
var filter = "storeid:abc, status:abed, blabhbalh ";
|
||||
var search = new SearchString(filter);
|
||||
Assert.Equal("storeid:abc, status:abed, blabhbalh", search.ToString());
|
||||
Assert.Equal("blabhbalh", search.TextSearch);
|
||||
Assert.Single(search.Filters["storeid"]);
|
||||
Assert.Single(search.Filters["status"]);
|
||||
Assert.Equal("abc", search.Filters["storeid"].First());
|
||||
Assert.Equal("abed", search.Filters["status"].First());
|
||||
Assert.Single(search.Filters["storeid"], "abc");
|
||||
Assert.Single(search.Filters["status"], "abed");
|
||||
|
||||
filter = "status:abed, status:abed2";
|
||||
search = new SearchString(filter);
|
||||
|
@ -1064,6 +1063,48 @@ namespace BTCPayServer.Tests
|
|||
search = new SearchString(filter);
|
||||
Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First());
|
||||
Assert.Equal("hekki", search.TextSearch);
|
||||
|
||||
// modify search
|
||||
filter = $"status:settled,exceptionstatus:paidLate,unusual:true, fulltext searchterm, storeid:{storeId},startdate:2019-04-25 01:00:00";
|
||||
search = new SearchString(filter);
|
||||
Assert.Equal(filter, search.ToString());
|
||||
Assert.Equal("fulltext searchterm", search.TextSearch);
|
||||
Assert.Single(search.Filters["storeid"], storeId);
|
||||
Assert.Single(search.Filters["status"], "settled");
|
||||
Assert.Single(search.Filters["exceptionstatus"], "paidLate");
|
||||
Assert.Single(search.Filters["unusual"], "true");
|
||||
|
||||
// toggle off bool with same value
|
||||
var modified = new SearchString(search.Toggle("unusual", "true"));
|
||||
Assert.Null(modified.GetFilterBool("unusual"));
|
||||
|
||||
// add to array
|
||||
modified = new SearchString(modified.Toggle("status", "processing"));
|
||||
var statusArray = modified.GetFilterArray("status");
|
||||
Assert.Equal(2, statusArray.Length);
|
||||
Assert.Contains("processing", statusArray);
|
||||
Assert.Contains("settled", statusArray);
|
||||
|
||||
// toggle off array with same value
|
||||
modified = new SearchString(modified.Toggle("status", "settled"));
|
||||
statusArray = modified.GetFilterArray("status");
|
||||
Assert.Single(statusArray, "processing");
|
||||
|
||||
// toggle off array with null value
|
||||
modified = new SearchString(modified.Toggle("status", null));
|
||||
Assert.Null(modified.GetFilterArray("status"));
|
||||
|
||||
// toggle off date with null value
|
||||
modified = new SearchString(modified.Toggle("startdate", "-7d"));
|
||||
Assert.Single(modified.GetFilterArray("startdate"), "-7d");
|
||||
modified = new SearchString(modified.Toggle("startdate", null));
|
||||
Assert.Null(modified.GetFilterArray("startdate"));
|
||||
|
||||
// toggle off date with same value
|
||||
modified = new SearchString(modified.Toggle("enddate", "-7d"));
|
||||
Assert.Single(modified.GetFilterArray("enddate"), "-7d");
|
||||
modified = new SearchString(modified.Toggle("enddate", "-7d"));
|
||||
Assert.Null(modified.GetFilterArray("enddate"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
@ -720,8 +720,8 @@ namespace BTCPayServer.Tests
|
|||
Assert.DoesNotContain(invoiceId, s.Driver.PageSource);
|
||||
|
||||
// unarchive via list
|
||||
s.Driver.FindElement(By.Id("SearchOptionsToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("SearchOptionsIncludeArchived")).Click();
|
||||
s.Driver.FindElement(By.Id("StatusOptionsToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("StatusOptionsIncludeArchived")).Click();
|
||||
Assert.Contains(invoiceId, s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click();
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
|
||||
|
|
|
@ -1073,34 +1073,44 @@ namespace BTCPayServer.Controllers
|
|||
public async Task<IActionResult> ListInvoices(InvoicesModel? model = null)
|
||||
{
|
||||
model = this.ParseListQuery(model ?? new InvoicesModel());
|
||||
var fs = new SearchString(model.SearchTerm);
|
||||
var timezoneOffset = model.TimezoneOffset ?? 0;
|
||||
var searchTerm = string.IsNullOrEmpty(model.SearchText) ? model.SearchTerm : $"{model.SearchText},{model.SearchTerm}";
|
||||
var fs = new SearchString(searchTerm, timezoneOffset);
|
||||
string? storeId = model.StoreId;
|
||||
var storeIds = new HashSet<string>();
|
||||
if (fs.GetFilterArray("storeid") is string[] l)
|
||||
if (storeId is not null)
|
||||
{
|
||||
storeIds.Add(storeId);
|
||||
}
|
||||
if (fs.GetFilterArray("storeid") is { } l)
|
||||
{
|
||||
foreach (var i in l)
|
||||
storeIds.Add(i);
|
||||
}
|
||||
if (storeId is not null)
|
||||
{
|
||||
storeIds.Add(storeId);
|
||||
model.StoreId = storeId;
|
||||
}
|
||||
model.StoreIds = storeIds.ToArray();
|
||||
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(model.SearchTerm, model.TimezoneOffset ?? 0);
|
||||
invoiceQuery.StoreId = model.StoreIds;
|
||||
model.Search = fs;
|
||||
model.SearchText = fs.TextSearch;
|
||||
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, timezoneOffset);
|
||||
invoiceQuery.StoreId = storeIds.ToArray();
|
||||
invoiceQuery.Take = model.Count;
|
||||
invoiceQuery.Skip = model.Skip;
|
||||
invoiceQuery.IncludeRefunds = true;
|
||||
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
|
||||
|
||||
model.IncludeArchived = invoiceQuery.IncludeArchived;
|
||||
// Apps
|
||||
var apps = await _appService.GetAllApps(GetUserId(), false, storeId);
|
||||
model.Apps = apps.Select(a => new InvoiceAppModel
|
||||
{
|
||||
Id = a.Id,
|
||||
AppName = a.AppName,
|
||||
AppType = a.AppType,
|
||||
AppOrderId = AppService.GetAppOrderId(a.AppType, a.Id)
|
||||
}).ToList();
|
||||
|
||||
foreach (var invoice in list)
|
||||
{
|
||||
var state = invoice.GetInvoiceState();
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
model.Invoices.Add(new InvoiceModel
|
||||
{
|
||||
Status = state,
|
||||
ShowCheckout = invoice.Status == InvoiceStatusLegacy.New,
|
||||
|
@ -1119,10 +1129,9 @@ namespace BTCPayServer.Controllers
|
|||
return View(model);
|
||||
}
|
||||
|
||||
private InvoiceQuery GetInvoiceQuery(string? searchTerm = null, int timezoneOffset = 0)
|
||||
private InvoiceQuery GetInvoiceQuery(SearchString fs, int timezoneOffset = 0)
|
||||
{
|
||||
var fs = new SearchString(searchTerm);
|
||||
var invoiceQuery = new InvoiceQuery()
|
||||
return new InvoiceQuery
|
||||
{
|
||||
TextSearch = fs.TextSearch,
|
||||
UserId = GetUserId(),
|
||||
|
@ -1136,7 +1145,6 @@ namespace BTCPayServer.Controllers
|
|||
StartDate = fs.GetFilterDate("startdate", timezoneOffset),
|
||||
EndDate = fs.GetFilterDate("enddate", timezoneOffset)
|
||||
};
|
||||
return invoiceQuery;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
@ -1147,17 +1155,17 @@ namespace BTCPayServer.Controllers
|
|||
var model = new InvoiceExport(_CurrencyNameTable);
|
||||
var fs = new SearchString(searchTerm);
|
||||
var storeIds = new HashSet<string>();
|
||||
if (fs.GetFilterArray("storeid") is string[] l)
|
||||
{
|
||||
foreach (var i in l)
|
||||
storeIds.Add(i);
|
||||
}
|
||||
if (storeId is not null)
|
||||
{
|
||||
storeIds.Add(storeId);
|
||||
}
|
||||
if (fs.GetFilterArray("storeid") is { } l)
|
||||
{
|
||||
foreach (var i in l)
|
||||
storeIds.Add(i);
|
||||
}
|
||||
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset);
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, timezoneOffset);
|
||||
invoiceQuery.StoreId = storeIds.ToArray();
|
||||
invoiceQuery.Skip = 0;
|
||||
invoiceQuery.Take = int.MaxValue;
|
||||
|
|
|
@ -58,6 +58,7 @@ namespace BTCPayServer.Controllers
|
|||
private readonly InvoiceActivator _invoiceActivator;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly AppService _appService;
|
||||
|
||||
public WebhookSender WebhookNotificationManager { get; }
|
||||
|
||||
|
@ -81,6 +82,7 @@ namespace BTCPayServer.Controllers
|
|||
UIWalletsController walletsController,
|
||||
InvoiceActivator invoiceActivator,
|
||||
LinkGenerator linkGenerator,
|
||||
AppService appService,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_displayFormatter = displayFormatter;
|
||||
|
@ -102,6 +104,7 @@ namespace BTCPayServer.Controllers
|
|||
_invoiceActivator = invoiceActivator;
|
||||
_linkGenerator = linkGenerator;
|
||||
_authorizationService = authorizationService;
|
||||
_appService = appService;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ namespace BTCPayServer.Controllers
|
|||
model = this.ParseListQuery(model ?? new ListPaymentRequestsViewModel());
|
||||
|
||||
var store = GetCurrentStore();
|
||||
var includeArchived = new SearchString(model.SearchTerm).GetFilterBool("includearchived") == true;
|
||||
var includeArchived = new SearchString(model.SearchTerm, model.TimezoneOffset ?? 0).GetFilterBool("includearchived") == true;
|
||||
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery
|
||||
{
|
||||
UserId = GetUserId(),
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
public class InvoicesModel : BasePagingViewModel
|
||||
{
|
||||
public List<InvoiceModel> Invoices { get; set; } = new List<InvoiceModel>();
|
||||
public List<InvoiceModel> Invoices { get; set; } = new ();
|
||||
public override int CurrentPageCount => Invoices.Count;
|
||||
public string[] StoreIds { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public bool IncludeArchived { get; set; }
|
||||
|
||||
public string SearchText { get; set; }
|
||||
public SearchString Search { get; set; }
|
||||
public List<InvoiceAppModel> Apps { get; set; }
|
||||
}
|
||||
|
||||
public class InvoiceModel
|
||||
|
@ -34,4 +35,12 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
public InvoiceDetailsModel Details { get; set; }
|
||||
public bool HasRefund { get; set; }
|
||||
}
|
||||
|
||||
public class InvoiceAppModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string AppName { get; set; }
|
||||
public string AppType { get; set; }
|
||||
public string AppOrderId { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,73 +2,129 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using ExchangeSharp;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class SearchString
|
||||
{
|
||||
readonly string _OriginalString;
|
||||
public SearchString(string str)
|
||||
private const char FilterSeparator = ',';
|
||||
private const char ValueSeparator = ':';
|
||||
|
||||
private readonly string _originalString;
|
||||
private readonly int _timezoneOffset;
|
||||
|
||||
public SearchString(string str, int timezoneOffset = 0)
|
||||
{
|
||||
str = str ?? string.Empty;
|
||||
str ??= string.Empty;
|
||||
str = str.Trim();
|
||||
_OriginalString = str.Trim();
|
||||
TextSearch = _OriginalString;
|
||||
var splitted = str.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
_originalString = str;
|
||||
_timezoneOffset = timezoneOffset;
|
||||
TextSearch = _originalString;
|
||||
var splitted = str.Split(new [] { FilterSeparator }, StringSplitOptions.RemoveEmptyEntries);
|
||||
Filters
|
||||
= splitted
|
||||
.Select(t => t.Split(new char[] { ':' }, 2, StringSplitOptions.RemoveEmptyEntries))
|
||||
.Select(t => t.Split(new [] { ValueSeparator }, 2, StringSplitOptions.RemoveEmptyEntries))
|
||||
.Where(kv => kv.Length == 2)
|
||||
.Select(kv => new KeyValuePair<string, string>(kv[0].ToLowerInvariant().Trim(), kv[1]))
|
||||
.Select(kv => new KeyValuePair<string, string>(UnifyKey(kv[0]), kv[1]))
|
||||
.ToMultiValueDictionary(o => o.Key, o => o.Value);
|
||||
|
||||
var val = splitted.FirstOrDefault(a => a?.IndexOf(':', StringComparison.OrdinalIgnoreCase) == -1);
|
||||
if (val != null)
|
||||
TextSearch = val.Trim();
|
||||
else
|
||||
TextSearch = "";
|
||||
var val = splitted.FirstOrDefault(a => a.IndexOf(ValueSeparator, StringComparison.OrdinalIgnoreCase) == -1);
|
||||
TextSearch = val != null ? val.Trim() : string.Empty;
|
||||
}
|
||||
|
||||
public string TextSearch { get; private set; }
|
||||
|
||||
public MultiValueDictionary<string, string> Filters { get; private set; }
|
||||
public MultiValueDictionary<string, string> Filters { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _OriginalString;
|
||||
return _originalString;
|
||||
}
|
||||
|
||||
internal string[] GetFilterArray(string key)
|
||||
public string Toggle(string key, string value)
|
||||
{
|
||||
key = UnifyKey(key);
|
||||
var keyValue = $"{key}{ValueSeparator}{value}";
|
||||
var prependOnInsert = string.IsNullOrEmpty(ToString()) ? string.Empty : $"{ToString()}{FilterSeparator}";
|
||||
if (!ContainsFilter(key)) return Finalize($"{prependOnInsert}{keyValue}");
|
||||
|
||||
var boolFilter = GetFilterBool(key);
|
||||
if (boolFilter != null)
|
||||
{
|
||||
return Finalize(ToString().Replace(keyValue, string.Empty));
|
||||
}
|
||||
|
||||
var dateFilter = GetFilterDate(key, _timezoneOffset);
|
||||
if (dateFilter != null)
|
||||
{
|
||||
var current = GetFilterArray(key).First();
|
||||
var oldValue = $"{key}{ValueSeparator}{current}";
|
||||
var newValue = string.IsNullOrEmpty(value) || current == value ? string.Empty : keyValue;
|
||||
return Finalize(_originalString.Replace(oldValue, newValue));
|
||||
}
|
||||
|
||||
var arrayFilter = GetFilterArray(key);
|
||||
if (arrayFilter != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return Finalize(arrayFilter.Aggregate(ToString(), (current, filter) =>
|
||||
current.Replace($"{key}{ValueSeparator}{filter}", string.Empty)));
|
||||
}
|
||||
return Finalize(arrayFilter.Contains(value)
|
||||
? ToString().Replace(keyValue, string.Empty)
|
||||
: $"{prependOnInsert}{keyValue}"
|
||||
);
|
||||
}
|
||||
|
||||
return Finalize(ToString());
|
||||
}
|
||||
|
||||
public string WithoutSearchText()
|
||||
{
|
||||
return string.IsNullOrEmpty(TextSearch)
|
||||
? Finalize(ToString())
|
||||
: Finalize(ToString()).Replace(TextSearch, string.Empty);
|
||||
}
|
||||
|
||||
public string[] GetFilterArray(string key)
|
||||
{
|
||||
key = UnifyKey(key);
|
||||
return Filters.ContainsKey(key) ? Filters[key].ToArray() : null;
|
||||
}
|
||||
|
||||
internal bool? GetFilterBool(string key)
|
||||
public bool? GetFilterBool(string key)
|
||||
{
|
||||
key = UnifyKey(key);
|
||||
if (!Filters.ContainsKey(key))
|
||||
return null;
|
||||
|
||||
return bool.TryParse(Filters[key].First(), out var r) ?
|
||||
r : (bool?)null;
|
||||
return bool.TryParse(Filters[key].First(), out var r) ? r : null;
|
||||
}
|
||||
|
||||
internal DateTimeOffset? GetFilterDate(string key, int timezoneOffset)
|
||||
public DateTimeOffset? GetFilterDate(string key, int timezoneOffset)
|
||||
{
|
||||
key = UnifyKey(key);
|
||||
if (!Filters.ContainsKey(key))
|
||||
return null;
|
||||
|
||||
var val = Filters[key].First();
|
||||
|
||||
// handle special string values
|
||||
if (val == "-24h")
|
||||
return DateTimeOffset.UtcNow.AddHours(-24).AddMinutes(timezoneOffset);
|
||||
else if (val == "-3d")
|
||||
return DateTimeOffset.UtcNow.AddDays(-3).AddMinutes(timezoneOffset);
|
||||
else if (val == "-7d")
|
||||
return DateTimeOffset.UtcNow.AddDays(-7).AddMinutes(timezoneOffset);
|
||||
switch (val)
|
||||
{
|
||||
// handle special string values
|
||||
case "-24h":
|
||||
case "-1d":
|
||||
return DateTimeOffset.UtcNow.AddDays(-1).AddMinutes(timezoneOffset);
|
||||
case "-3d":
|
||||
return DateTimeOffset.UtcNow.AddDays(-3).AddMinutes(timezoneOffset);
|
||||
case "-7d":
|
||||
return DateTimeOffset.UtcNow.AddDays(-7).AddMinutes(timezoneOffset);
|
||||
}
|
||||
|
||||
// default parsing logic
|
||||
var success = DateTimeOffset.TryParse(val, null as IFormatProvider, DateTimeStyles.AssumeUniversal, out var r);
|
||||
var success = DateTimeOffset.TryParse(val, null, DateTimeStyles.AssumeUniversal, out var r);
|
||||
if (success)
|
||||
{
|
||||
r = r.AddMinutes(timezoneOffset);
|
||||
|
@ -78,6 +134,20 @@ namespace BTCPayServer
|
|||
return null;
|
||||
}
|
||||
|
||||
internal bool ContainsFilter(string key) => Filters.ContainsKey(key);
|
||||
public bool ContainsFilter(string key)
|
||||
{
|
||||
return Filters.ContainsKey(UnifyKey(key));
|
||||
}
|
||||
|
||||
private string UnifyKey(string key)
|
||||
{
|
||||
return key.ToLowerInvariant().Trim();
|
||||
}
|
||||
|
||||
private static string Finalize(string str)
|
||||
{
|
||||
var value = str.TrimStart(FilterSeparator).TrimEnd(FilterSeparator);
|
||||
return string.IsNullOrEmpty(value) ? " " : value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,25 @@
|
|||
@model InvoicesModel
|
||||
@{
|
||||
ViewData.SetActivePage(InvoiceNavPages.Index, "Invoices");
|
||||
var storeIds = string.Join("", Model.StoreIds.Select(storeId => $",storeid:{storeId}"));
|
||||
if (this.Context.GetRouteValue("storeId") is string)
|
||||
storeIds = string.Empty;
|
||||
|
||||
var statusFilterCount = CountArrayFilter("status") + CountArrayFilter("exceptionstatus") + (HasBooleanFilter("includearchived") ? 1 : 0) + (HasBooleanFilter("unusual") ? 1 : 0);
|
||||
var hasDateFilter = HasArrayFilter("startdate") || HasArrayFilter("enddate");
|
||||
var appFilterCount = Model.Apps.Count(app => HasArrayFilter("orderid", app.AppOrderId));
|
||||
}
|
||||
|
||||
@functions
|
||||
{
|
||||
private int CountArrayFilter(string type) =>
|
||||
Model.Search.ContainsFilter(type) ? Model.Search.GetFilterArray(type).Length : 0;
|
||||
|
||||
private bool HasArrayFilter(string type, string key = null) =>
|
||||
Model.Search.ContainsFilter(type) && (key is null || Model.Search.GetFilterArray(type).Contains(key));
|
||||
|
||||
private bool HasBooleanFilter(string key) =>
|
||||
Model.Search.ContainsFilter(key) && Model.Search.GetFilterBool(key) is true;
|
||||
|
||||
private bool HasCustomDateFilter() =>
|
||||
Model.Search.ContainsFilter("startdate") && Model.Search.ContainsFilter("enddate");
|
||||
}
|
||||
|
||||
@section PageHeadContent
|
||||
|
@ -16,6 +32,16 @@
|
|||
.invoice-payments {
|
||||
padding-left: var(--btcpay-space-l);
|
||||
}
|
||||
.dropdown > .btn {
|
||||
min-width: 7rem;
|
||||
padding-left: 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
@@media (max-width: 568px) {
|
||||
#SearchText {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
|
@ -59,7 +85,7 @@
|
|||
|
||||
var dtpStartDate = $("#dtpStartDate").val();
|
||||
if (dtpStartDate !== null && dtpStartDate !== "") {
|
||||
filterString = "startDate%3A" + dtpStartDate;
|
||||
filterString = "startdate%3A" + dtpStartDate;
|
||||
}
|
||||
|
||||
var dtpEndDate = $("#dtpEndDate").val();
|
||||
|
@ -67,7 +93,7 @@
|
|||
if (filterString !== "") {
|
||||
filterString += ",";
|
||||
}
|
||||
filterString += "endDate%3A" + dtpEndDate;
|
||||
filterString += "enddate%3A" + dtpEndDate;
|
||||
}
|
||||
|
||||
if (filterString !== "") {
|
||||
|
@ -143,6 +169,14 @@
|
|||
<div class="flex-fill">
|
||||
<p class="mb-2">Invoices are documents issued by the seller to a buyer to collect payment.</p>
|
||||
<p class="mb-3">An invoice must be paid within a defined time interval at a fixed exchange rate to protect the issuer from price fluctuations.</p>
|
||||
<p class="mb-3">
|
||||
You can also apply filters to your search by searching for <code>filtername:value</code>.
|
||||
Be sure to split your search parameters with comma. Supported filters are:
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>orderid:id</code> for filtering a specific order</li>
|
||||
<li><code>itemcode:code</code> for filtering a specific type of item purchased through the pos or crowdfund apps</li>
|
||||
</ul>
|
||||
<a href="https://docs.btcpayserver.org/Invoices/" target="_blank" rel="noreferrer noopener">Learn More</a>
|
||||
</div>
|
||||
<button type="button" class="btn-close ms-auto" data-bs-toggle="collapse" data-bs-target="#descriptor" aria-expanded="false" aria-label="Close">
|
||||
|
@ -200,75 +234,103 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="help" class="row collapse">
|
||||
<div class="col-xl-8 pb-3">
|
||||
<p>
|
||||
You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.
|
||||
Be sure to split your search parameters with comma, for example:<br />
|
||||
<code>startdate:2019-04-25 13:00:00, status:paid</code>
|
||||
</p>
|
||||
<p class="mb-2">
|
||||
You can also apply filters to your search by searching for <code>filtername:value</code>, supported filters are:
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>storeid:id</code> for filtering a specific store</li>
|
||||
<li><code>orderid:id</code> for filtering a specific order</li>
|
||||
<li><code>itemcode:code</code> for filtering a specific type of item purchased through the pos or crowdfund apps</li>
|
||||
<li><code>status:(expired|invalid|complete|confirmed|paid|new)</code> for filtering a specific status</li>
|
||||
<li><code>exceptionstatus:(paidover|paidlate|paidpartial)</code> for filtering a specific exception state</li>
|
||||
<li><code>unusual:(true|false)</code> for filtering invoices which might requires merchant attention (those invalid or with an exceptionstatus)</li>
|
||||
<li><code>startdate:yyyy-MM-dd HH:mm:ss</code> getting invoices that were created after certain date</li>
|
||||
<li><code>enddate:yyyy-MM-dd HH:mm:ss</code> getting invoices that were created before certain date</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<form class="@(Model.Invoices.Count > 0 ? "col-xl-7 col-xxl-8 " : "")mb-4" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" method="get">
|
||||
<input type="hidden" asp-for="Count" />
|
||||
<form class="d-flex flex-wrap flex-sm-nowrap align-items-center gap-3 mb-4 @(Model.Invoices.Any() ? "col-xl-7 col-xxl-8" : null)" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" method="get">
|
||||
<input asp-for="Count" type="hidden" />
|
||||
<input asp-for="TimezoneOffset" type="hidden" />
|
||||
<div class="input-group">
|
||||
<a href="#help" class="input-group-text text-secondary text-decoration-none" data-bs-toggle="collapse">
|
||||
<span class="fa fa-filter"></span>
|
||||
</a>
|
||||
<input asp-for="SearchTerm" class="form-control" />
|
||||
<button type="submit" class="btn btn-secondary" title="Search invoice">
|
||||
<span class="fa fa-search"></span> Search
|
||||
<input asp-for="SearchTerm" type="hidden" value="@Model.Search.WithoutSearchText()"/>
|
||||
<input asp-for="SearchText" class="form-control" placeholder="Search…" />
|
||||
<div class="dropdown">
|
||||
<button id="StatusOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@if (statusFilterCount > 0)
|
||||
{
|
||||
<span>@statusFilterCount Status</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>All Status</span>
|
||||
}
|
||||
</button>
|
||||
<button type="button" id="SearchOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="SearchOptionsToggle">
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="status:invalid@{@storeIds}">Invalid Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="status:processing,status:settled@{@storeIds}">Settled Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidLate@{@storeIds}">Settled Late Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidPartial@{@storeIds}">Settled Partial Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidOver@{@storeIds}">Settled Over Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="unusual:true@{@storeIds}">Unusual Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="includearchived:true@{@storeIds}" id="SearchOptionsIncludeArchived">Archived Invoices</a>
|
||||
<div role="separator" class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-timezoneoffset="0" asp-route-searchTerm="startDate:-24h@{@storeIds}">Last 24 hours</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-timezoneoffset="0" asp-route-searchTerm="startDate:-3d@{@storeIds}">Last 3 days</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-timezoneoffset="0" asp-route-searchTerm="startDate:-7d@{@storeIds}">Last 7 days</a>
|
||||
<button type="button" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#customRangeModal">Custom Range</button>
|
||||
<div role="separator" class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="?searchTerm=">Unfiltered</a>
|
||||
<div class="dropdown-menu" aria-labelledby="StatusOptionsToggle">
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("status", "invalid")" class="dropdown-item @(HasArrayFilter("status", "invalid") ? "custom-active" : "")">Invalid</a>
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("status", "processing")" class="dropdown-item @(HasArrayFilter("status", "processing") ? "custom-active" : "")">Processing</a>
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("status", "settled")" class="dropdown-item @(HasArrayFilter("status", "settled") ? "custom-active" : "")">Settled</a>
|
||||
<hr class="dropdown-divider">
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("exceptionstatus", "paidLate")" class="dropdown-item @(HasArrayFilter("exceptionstatus", "paidLate") ? "custom-active" : "")">Settled Late</a>
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("exceptionstatus", "paidPartial")" class="dropdown-item @(HasArrayFilter("exceptionstatus", "paidPartial") ? "custom-active" : "")">Settled Partial</a>
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("exceptionstatus", "paidOver")" class="dropdown-item @(HasArrayFilter("exceptionstatus", "paidOver") ? "custom-active" : "")">Settled Over</a>
|
||||
<hr class="dropdown-divider">
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("unusual", "true")" class="dropdown-item @(HasBooleanFilter("unusual") ? "custom-active" : "")">Unusual</a>
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("includearchived", "true")" class="dropdown-item @(HasBooleanFilter("includearchived") ? "custom-active" : "")" id="StatusOptionsIncludeArchived">Archived</a>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.Apps.Any())
|
||||
{
|
||||
<div class="dropdown">
|
||||
<button id="AppOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@if (appFilterCount > 0)
|
||||
{
|
||||
<span>@appFilterCount Plugin@(appFilterCount > 1 ? "s" : "")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>All Plugins</span>
|
||||
}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="AppOptionsToggle">
|
||||
@foreach (var app in Model.Apps)
|
||||
{
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("orderid", app.AppOrderId)" class="dropdown-item @(HasArrayFilter("orderid", app.AppOrderId) ? "custom-active" : "")">@app.AppName</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="dropdown">
|
||||
<button id="DateOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@if (hasDateFilter)
|
||||
{
|
||||
if (HasArrayFilter("startdate", "-1d"))
|
||||
{
|
||||
<span>24 Hours</span>
|
||||
}
|
||||
else if (HasArrayFilter("startdate", "-3d"))
|
||||
{
|
||||
<span>3 Days</span>
|
||||
}
|
||||
else if (HasArrayFilter("startdate", "-7d"))
|
||||
{
|
||||
<span>7 Days</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Custom</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>All Time</span>
|
||||
}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="DateOptionsToggle">
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("startdate", "-1d")" class="dropdown-item @(HasArrayFilter("startdate", "-1d") ? "custom-active" : "")">Last 24 hours</a>
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("startdate", "-3d")" class="dropdown-item @(HasArrayFilter("startdate", "-3d") ? "custom-active" : "")">Last 3 days</a>
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("startdate", "-7d")" class="dropdown-item @(HasArrayFilter("startdate", "-7d") ? "custom-active" : "")">Last 7 days</a>
|
||||
<button type="button" class="dropdown-item @(HasCustomDateFilter() ? "custom-active" : "")" data-bs-toggle="modal" data-bs-target="#customRangeModal">Custom Range</button>
|
||||
</div>
|
||||
</div>
|
||||
<span asp-validation-for="SearchTerm" class="text-danger"></span>
|
||||
</form>
|
||||
|
||||
@if (Model.Invoices.Count > 0)
|
||||
@if (Model.Invoices.Any())
|
||||
{
|
||||
<form method="post" id="MassAction" asp-action="MassAction" class="">
|
||||
<div class="d-inline-flex align-items-center pb-2 float-xl-end mb-2 gap-3">
|
||||
<input type="hidden" name="storeId" value="@Model.StoreId" />
|
||||
<div class="dropdown order-xl-1">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Actions
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-xl-end" aria-labelledby="ActionsDropdownToggle">
|
||||
<button type="submit" class="dropdown-item" name="command" value="archive" id="ActionsDropdownArchive">Archive</button>
|
||||
@if (Model.IncludeArchived)
|
||||
@if (HasBooleanFilter("includearchived"))
|
||||
{
|
||||
<button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="unarchive" id="ActionsDropdownUnarchive">Unarchive</button>
|
||||
}
|
||||
|
@ -276,7 +338,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="dropdown d-inline-flex align-items-center gap-3">
|
||||
<button class="btn btn-secondary dropdown-toggle order-xl-1" type="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret order-xl-1" type="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Export
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="ExportDropdownToggle">
|
||||
|
|
|
@ -11028,6 +11028,19 @@ fieldset:disabled .btn {
|
|||
[list]::-webkit-list-button {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.dropdown-toggle.dropdown-toggle-custom-caret {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%236E7681' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.75rem center;
|
||||
background-size: 16px 12px;
|
||||
padding-right: 2.25rem;
|
||||
}
|
||||
|
||||
.dropdown-toggle.dropdown-toggle-custom-caret::after {
|
||||
border: none;
|
||||
margin-left: var(--btcpay-space-s);
|
||||
}
|
||||
/* Scrollbar - first works on Firefox, rest on WebKit-based browsers */
|
||||
* {
|
||||
--btcpay-scrollbar-width: 0.375rem;
|
||||
|
@ -11235,6 +11248,22 @@ ul:not([class]) li {
|
|||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.dropdown .custom-active {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--btcpay-space-s);
|
||||
}
|
||||
|
||||
.dropdown .custom-active::after {
|
||||
display: inline-block;
|
||||
margin-left: auto;
|
||||
content: '';
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
background-color: var(--btcpay-body-bg-active);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.font-monospace {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
:root {
|
||||
--btcpay-neutral-50: #0D1117;
|
||||
--btcpay-neutral-50: #0d1117;
|
||||
--btcpay-neutral-100: var(--btcpay-neutral-dark-900);
|
||||
--btcpay-neutral-200: var(--btcpay-neutral-dark-800);
|
||||
--btcpay-neutral-300: var(--btcpay-neutral-dark-700);
|
||||
|
|
Loading…
Add table
Reference in a new issue