Update Invoice Views (#3264)

* updates create invoice

* updates invoice list

* formats

* updates row

* updates

* Improve invoice list markup and fix mass action form

* Responsive invoice table

* Improve spacing on invoice detail view

* Improve archive message

* Responsive status change partial

* Add test case for mass archiving

* Add mass unarchiving

Closes #3270.

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
dstrukt 2022-01-11 00:14:34 -08:00 committed by GitHub
parent 3c5d809cf9
commit 4b941a5145
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 362 additions and 335 deletions

View File

@ -393,6 +393,7 @@ namespace BTCPayServer.Tests
var storeUrl = s.Driver.Url;
s.ClickOnAllSectionLinks();
s.GoToInvoices();
Assert.Contains("There are no invoices matching your criteria.", s.Driver.PageSource);
var invoiceId = s.CreateInvoice();
s.FindAlertMessage();
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
@ -414,6 +415,23 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain("Unarchive", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text);
s.GoToInvoices();
Assert.Contains(invoiceId, s.Driver.PageSource);
// archive via list
s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownArchive")).Click();
Assert.Contains("1 invoice archived", s.FindAlertMessage().Text);
Assert.DoesNotContain(invoiceId, s.Driver.PageSource);
// unarchive via list
s.Driver.FindElement(By.Id("SearchOptionsToggle")).Click();
s.Driver.FindElement(By.Id("SearchOptionsIncludeArchived")).Click();
Assert.Contains(invoiceId, s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownUnarchive")).Click();
Assert.Contains("1 invoice unarchived", s.FindAlertMessage().Text);
Assert.Contains(invoiceId, s.Driver.PageSource);
// When logout out we should not be able to access store and invoice details
s.Logout();

View File

@ -437,8 +437,12 @@ namespace BTCPayServer.Controllers
{
case "archive":
await _InvoiceRepository.MassArchive(selectedItems);
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice(s) archived.";
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} archived.";
break;
case "unarchive":
await _InvoiceRepository.MassArchive(selectedItems, false);
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} unarchived.";
break;
}
}
@ -763,6 +767,8 @@ namespace BTCPayServer.Controllers
invoiceQuery.Skip = model.Skip;
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
model.IncludeArchived = invoiceQuery.IncludeArchived;
foreach (var invoice in list)
{
var state = invoice.GetInvoiceState();

View File

@ -10,6 +10,7 @@ namespace BTCPayServer.Models.InvoicingModels
public List<InvoiceModel> Invoices { get; set; } = new List<InvoiceModel>();
public string[] StoreIds { get; set; }
public string StoreId { get; set; }
public bool IncludeArchived { get; set; }
}
public class InvoiceModel

View File

@ -446,7 +446,7 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task MassArchive(string[] invoiceIds)
public async Task MassArchive(string[] invoiceIds, bool archive = true)
{
using (var context = _applicationDbContextFactory.CreateContext())
{
@ -458,7 +458,7 @@ namespace BTCPayServer.Services.Invoices
foreach (InvoiceData invoice in items)
{
invoice.Archived = true;
invoice.Archived = archive;
}
await context.SaveChangesAsync();

View File

@ -44,111 +44,112 @@
<select asp-for="StoreId" asp-items="Model.Stores" class="form-select"></select>
<span asp-validation-for="StoreId" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-4">Invoice Details</h4>
}
<div class="form-group">
<label asp-for="Amount" class="form-label"></label>
<input asp-for="Amount" class="form-control" />
<span asp-validation-for="Amount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Currency" class="form-label"></label>
<input asp-for="Currency" class="form-control" />
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="OrderId" class="form-label"></label>
<input asp-for="OrderId" class="form-control" />
<span asp-validation-for="OrderId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ItemDesc" class="form-label"></label>
<input asp-for="ItemDesc" class="form-control" />
<span asp-validation-for="ItemDesc" class="text-danger"></span>
</div>
<div class="form-group mb-4">
<label asp-for="SupportedTransactionCurrencies" class="form-label"></label>
@foreach (var item in Model.AvailablePaymentMethods)
{
<div class="form-check mb-2">
<label class="form-check-label">
<input name="SupportedTransactionCurrencies" class="form-check-input" checked="checked" type="checkbox" value="@item.Value">
@item.Text
</label>
</div>
}
<span asp-validation-for="SupportedTransactionCurrencies" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DefaultPaymentMethod" class="form-label"></label>
<select asp-for="DefaultPaymentMethod" asp-items="Model.AvailablePaymentMethods" class="form-select">
<option value="" selected>Use the stores default</option>
</select>
<span asp-validation-for="DefaultPaymentMethod" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-4">Customer Information</h4>
<div class="form-group">
<label asp-for="BuyerEmail" class="form-label"></label>
<input asp-for="BuyerEmail" class="form-control" />
<span asp-validation-for="BuyerEmail" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="RequiresRefundEmail" class="form-label"></label>
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select"></select>
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-2">Additional Options</h4>
<div class="form-group">
<div class="accordion" id="additional">
<div class="accordion-item">
<h2 class="accordion-header" id="additional-pos-data-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-pos-data" aria-expanded="false" aria-controls="additional-pos-data">
Point Of Sale Data
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-pos-data" class="accordion-collapse collapse" aria-labelledby="additional-pos-data-header">
<p>Custom data to correlate the invoice with an order. This data can be a simple text, number or JSON object, e.g. <code>{ "orderId": 615, "product": "Pizza" }</code></p>
<div class="form-group">
<label asp-for="PosData" class="form-label"></label>
<input asp-for="PosData" class="form-control"/>
<span asp-validation-for="PosData" class="text-danger"></span>
<div class="d-flex justify-content-between">
<div class="form-group flex-fill me-4">
<label asp-for="Amount" class="form-label"></label>
<input asp-for="Amount" class="form-control" />
<span asp-validation-for="Amount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Currency" class="form-label"></label>
<input asp-for="Currency" class="form-control" />
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="OrderId" class="form-label"></label>
<input asp-for="OrderId" class="form-control" />
<span asp-validation-for="OrderId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ItemDesc" class="form-label"></label>
<input asp-for="ItemDesc" class="form-control" />
<span asp-validation-for="ItemDesc" class="text-danger"></span>
</div>
<div class="form-group mb-4">
<label asp-for="SupportedTransactionCurrencies" class="form-label"></label>
@foreach (var item in Model.AvailablePaymentMethods)
{
<div class="form-check mb-2">
<label class="form-check-label">
<input name="SupportedTransactionCurrencies" class="form-check-input" checked="checked" type="checkbox" value="@item.Value">
@item.Text
</label>
</div>
}
<span asp-validation-for="SupportedTransactionCurrencies" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DefaultPaymentMethod" class="form-label"></label>
<select asp-for="DefaultPaymentMethod" asp-items="Model.AvailablePaymentMethods" class="form-select">
<option value="" selected>Use the stores default</option>
</select>
<span asp-validation-for="DefaultPaymentMethod" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-4">Customer Information</h4>
<div class="form-group">
<label asp-for="BuyerEmail" class="form-label"></label>
<input asp-for="BuyerEmail" class="form-control" />
<span asp-validation-for="BuyerEmail" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="RequiresRefundEmail" class="form-label"></label>
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select"></select>
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-2">Additional Options</h4>
<div class="form-group">
<div class="accordion" id="additional">
<div class="accordion-item">
<h2 class="accordion-header" id="additional-pos-data-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-pos-data" aria-expanded="false" aria-controls="additional-pos-data">
Point Of Sale Data
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-pos-data" class="accordion-collapse collapse" aria-labelledby="additional-pos-data-header">
<p>Custom data to correlate the invoice with an order. This data can be a simple text, number or JSON object, e.g. <code>{ "orderId": 615, "product": "Pizza" }</code></p>
<div class="form-group">
<label asp-for="PosData" class="form-label"></label>
<input asp-for="PosData" class="form-control" />
<span asp-validation-for="PosData" class="text-danger"></span>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="additional-notifications-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-notifications" aria-expanded="false" aria-controls="additional-notifications">
Invoice Notifications
<vc:icon symbol="caret-down"/>
</button>
</h2>
<div id="additional-notifications" class="accordion-collapse collapse" aria-labelledby="additional-notifications-header">
<div class="accordion-body">
<div class="form-group">
<label asp-for="NotificationUrl" class="form-label"></label>
<input asp-for="NotificationUrl" class="form-control"/>
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NotificationEmail" class="form-label"></label>
<input asp-for="NotificationEmail" class="form-control"/>
<span asp-validation-for="NotificationEmail" class="text-danger"></span>
<p id="InvoiceEmailHelpBlock" class="form-text text-muted">
Receive updates for this invoice.
</p>
<div class="accordion-item">
<h2 class="accordion-header" id="additional-notifications-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-notifications" aria-expanded="false" aria-controls="additional-notifications">
Invoice Notifications
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-notifications" class="accordion-collapse collapse" aria-labelledby="additional-notifications-header">
<div class="accordion-body">
<div class="form-group">
<label asp-for="NotificationUrl" class="form-label"></label>
<input asp-for="NotificationUrl" class="form-control" />
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NotificationEmail" class="form-label"></label>
<input asp-for="NotificationEmail" class="form-control" />
<span asp-validation-for="NotificationEmail" class="text-danger"></span>
<p id="InvoiceEmailHelpBlock" class="form-text text-muted">
Receive updates for this invoice.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="form-group mt-4">
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
</div>
<div class="form-group mt-4">
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
</div>
</form>
</div>
</div>

View File

@ -314,7 +314,7 @@
@if (Model.Deliveries.Count != 0)
{
<h3 class="mb-3">Webhook deliveries</h3>
<h3 class="mb-3 mt-4">Webhook deliveries</h3>
<ul class="list-group mb-5">
@foreach (var delivery in Model.Deliveries)
{
@ -369,7 +369,7 @@
}
</ul>
}
<div class="row">
<div class="row mt-4">
<div class="col-md-12">
<h3 class="mb-0">Events</h3>
<table class="table table-hover table-responsive-md">

View File

@ -11,8 +11,8 @@
<h5 class="alert-heading">Updated in v1.4.0</h5>
<p class="mb-2">Invoice states have been updated to match the Greenfield API:</p>
<div class="row">
<div class="col col-12 col-sm-6">
<ul class="list-unstyled mb-sm-0">
<div class="col-12 col-md-6">
<ul class="list-unstyled mb-md-0">
<li>
<span class="badge badge-processing">Paid</span>
<span class="mx-1">is now shown as</span>
@ -30,7 +30,7 @@
</li>
</ul>
</div>
<div class="col col-12 col-sm-6 d-flex justify-content-sm-end align-items-sm-end">
<div class="col-12 col-md-6 d-flex justify-content-md-end align-items-md-end">
<button name="command" type="submit" value="save" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="alert">Don't Show Again</button>
</div>
</div>

View File

@ -6,7 +6,7 @@
}
@section PageHeadContent {
<style type="text/css">
<style>
.invoice-payments {
padding-left: 2rem;
}
@ -16,14 +16,6 @@
font-weight: bold;
}
.wraptext200 {
max-width: 200px;
text-overflow: ellipsis;
overflow: hidden;
display: block;
white-space: nowrap;
}
.pavpill {
display: inline-block;
padding: 0.3em 0.5em;
@ -59,16 +51,23 @@
background: #c94a47;
color: #fff;
}
.badge-processing {
background: #f1c332;
color: #000;
}
.badge-settled {
background: #329f80;
color: #fff;
}
/* pull mass action form up, so that it is besides the search form */
@@media (min-width: 992px) {
#MassAction {
margin-top: -4rem;
}
}
</style>
}
@ -102,7 +101,7 @@
alert("Invoice state update failed");
});
})
delegate('click', '.showInvoice', e => {
e.preventDefault();
const { invoiceId } = e.target.dataset;
@ -135,7 +134,7 @@
$("#dtpStartDate").next().trigger("focus");
}
})
function getDateStringWithOffset(hoursDiff) {
var datenow = new Date();
var newDate = new Date(datenow.getTime() - (hoursDiff * 60 * 60 * 1000));
@ -150,7 +149,7 @@
});
$("#invoices")
.on("click", ".invoice-row .invoice-details-toggle", function(e) {
.on("click", ".invoice-row .invoice-details-toggle", function (e) {
e.preventDefault();
e.stopPropagation(true);
@ -167,7 +166,7 @@
}
});
})
.on("click", ".invoice-row", function(e) {
.on("click", ".invoice-row", function (e) {
const $invoiceRow = $(e.currentTarget);
if (!$(e.target).is("a,.badge,.selector")) {
$invoiceRow.find(".selector").trigger("click");
@ -196,94 +195,55 @@
</a>
</div>
<partial name="InvoiceStatusChangePartial"/>
<partial name="InvoiceStatusChangePartial" />
<div class="row">
<div class="col-12 col-lg-6 mb-5 mb-lg-2 ms-auto">
<form asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" method="get">
<input type="hidden" asp-for="Count"/>
<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
@* Custom Range Modal *@
<div class="modal fade" id="customRangeModal" tabindex="-1" role="dialog" aria-labelledby="customRangeModalTitle" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered" role="document" style="max-width: 550px;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="customRangeModalTitle">Filter invoices by Custom Range</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<vc:icon symbol="close" />
</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}">Paid 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}">Paid 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}">Paid 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}">Paid 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}">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>
<div class="modal-body">
<div class="form-group row">
<label for="dtpStartDate" class="col-sm-3 col-form-label">Start Date</label>
<div class="col-sm-9">
<div class="input-group">
<input id="dtpStartDate" class="form-control flatdtpicker" type="datetime-local"
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
placeholder="Start Date" />
<button type="button" class="btn btn-primary input-group-clear" title="Clear">
<span class="fa fa-times"></span>
</button>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">End Date</label>
<div class="col-sm-9">
<div class="input-group">
<input id="dtpEndDate" class="form-control flatdtpicker" type="datetime-local"
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
placeholder="End Date" />
<button type="button" class="btn btn-primary input-group-clear" title="Clear">
<span class="fa fa-times"></span>
</button>
</div>
</div>
</div>
</div>
<span asp-validation-for="SearchTerm" class="text-danger"></span>
</form>
@* Custom Range Modal *@
<div class="modal fade" id="customRangeModal" tabindex="-1" role="dialog" aria-labelledby="customRangeModalTitle" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered" role="document" style="max-width: 550px;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="customRangeModalTitle">Filter invoices by Custom Range</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<vc:icon symbol="close" />
</button>
</div>
<div class="modal-body">
<div class="form-group row">
<label for="dtpStartDate" class="col-sm-3 col-form-label">Start Date</label>
<div class="col-sm-9">
<div class="input-group">
<input id="dtpStartDate" class="form-control flatdtpicker" type="datetime-local"
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
placeholder="Start Date" />
<button type="button" class="btn btn-primary input-group-clear" title="Clear">
<span class="fa fa-times"></span>
</button>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">End Date</label>
<div class="col-sm-9">
<div class="input-group">
<input id="dtpEndDate" class="form-control flatdtpicker" type="datetime-local"
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
placeholder="End Date" />
<button type="button" class="btn btn-primary input-group-clear" title="Clear">
<span class="fa fa-times"></span>
</button>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button id="btnCustomRangeDate" type="button" class="btn btn-primary">Filter</button>
</div>
</div>
<div class="modal-footer">
<button id="btnCustomRangeDate" type="button" class="btn btn-primary">Filter</button>
</div>
</div>
</div>
</div>
<div class="row collapse" id="help">
<div class="col @(Model.Total > 0 ? "pt-3 pb-lg-5" : "")">
<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 />
@ -304,17 +264,57 @@
</ul>
</div>
</div>
<form class="col-lg-6 col-xl-8 mb-4" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" method="get">
<input type="hidden" asp-for="Count" />
<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
</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>
@if (Model.Total > 0)
{
<form method="post" id="MassAction" asp-action="MassAction" class="mt-lg-n5">
<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}">Paid 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}">Paid 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}">Paid 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}">Paid 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>
</div>
<span asp-validation-for="SearchTerm" class="text-danger"></span>
</form>
<form method="post" id="MassAction" asp-action="MassAction" class="">
<div class="d-inline-flex align-items-center pb-2 float-lg-end mb-2">
<input type="hidden" name="storeId" value="@Model.StoreId" />
<a href="https://docs.btcpayserver.org/Accounting/" class="ms-2 ms-lg-0 me-lg-2 order-1 order-lg-0" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<span class="me-2">
<button class="btn btn-secondary dropdown-toggle mb-1" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
</button>
<div class="dropdown-menu" aria-labelledby="ActionsDropdownToggle">
<button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="archive"><i class="fa fa-archive"></i> Archive</button>
<button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="archive" id="ActionsDropdownArchive"><i class="fa fa-archive"></i> Archive</button>
@if (Model.IncludeArchived)
{
<button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="unarchive" id="ActionsDropdownUnarchive"><i class="fa fa-archive"></i> Unarchive</button>
}
</div>
</span>
<span>
@ -326,139 +326,140 @@
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="json" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">JSON</a>
</div>
</span>
</div>
<a href="https://docs.btcpayserver.org/Accounting/" class="ms-1" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<table id="invoices" class="table table-hover table-responsive-md mt-4">
<thead>
<tr>
<th style="width:2rem;" class="only-for-js">
<input id="selectAllCheckbox" type="checkbox" class="form-check-input"/>
</th>
<th style="min-width:90px;" class="col-md-auto">
Date
<a id="switchTimeFormat" href="#">
<span class="fa fa-clock-o" title="Switch date format"></span>
</a>
</th>
<th style="max-width: 180px;">OrderId</th>
<th>InvoiceId</th>
<th style="min-width: 150px;">Status</th>
<th style="text-align:right">Amount</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var invoice in Model.Invoices)
{
<tr id="invoice_@invoice.InvoiceId" class="invoice-row">
<td class="only-for-js">
<input name="selectedItems" type="checkbox" class="selector form-check-input" value="@invoice.InvoiceId"/>
</td>
<td>
<span class="switchTimeFormat" data-switch="@invoice.Date.ToTimeAgo()">
@invoice.Date.ToBrowserDate()
</span>
</td>
<td style="max-width: 180px;">
@if (invoice.RedirectUrl != string.Empty)
{
<a href="@invoice.RedirectUrl" class="wraptext200" rel="noreferrer noopener">@invoice.OrderId</a>
}
else
{
<span>@invoice.OrderId</span>
}
</td>
<td>@invoice.InvoiceId</td>
<td>
@if (invoice.Details.Archived)
{
<span class="badge bg-warning">archived</span>
}
@if (invoice.CanMarkStatus)
{
<div id="pavpill_@invoice.InvoiceId" class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
<span class="dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString() *@
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
{
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString())
;
}
@if (Model.Total > 0)
{
<div style="clear:both"></div>
<div class="table-responsive">
<table id="invoices" class="table table-hover">
<thead>
<tr>
<th style="width:2rem;" class="only-for-js">
<input id="selectAllCheckbox" type="checkbox" class="form-check-input" />
<th style="min-width:90px;" class="col-md-auto">
Date
<a id="switchTimeFormat" href="#">
<span class="fa fa-clock-o" title="Switch date format"></span>
</a>
</th>
<th class="text-nowrap">Order Id</th>
<th class="text-nowrap">Invoice Id</th>
<th>Status</th>
<th class="text-end">Amount</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var invoice in Model.Invoices)
{
<tr id="invoice_@invoice.InvoiceId" class="invoice-row">
<td class="only-for-js">
<input name="selectedItems" type="checkbox" class="selector form-check-input" value="@invoice.InvoiceId" />
</td>
<td>
<span class="switchTimeFormat" data-switch="@invoice.Date.ToTimeAgo()">
@invoice.Date.ToBrowserDate()
</span>
<div class="dropdown-menu pull-right">
@if (invoice.CanMarkInvalid)
{
<button class="dropdown-item small cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="invalid">
Mark as invalid <span class="fa fa-times"></span>
</button>
}
@if (invoice.CanMarkSettled)
{
<button class="dropdown-item small cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="settled">
Mark as settled <span class="fa fa-check-circle"></span>
</button>
}
</td>
<td style="max-width:120px;">
@if (invoice.RedirectUrl != string.Empty)
{
<a href="@invoice.RedirectUrl" class="wraptextAuto" rel="noreferrer noopener">@invoice.OrderId</a>
}
else
{
<span>@invoice.OrderId</span>
}
</td>
<td class="text-break">@invoice.InvoiceId</td>
<td>
@if (invoice.Details.Archived)
{
<span class="badge bg-warning">archived</span>
}
@if (invoice.CanMarkStatus)
{
<div id="pavpill_@invoice.InvoiceId" class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
<span class="dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString() *@
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
{
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString())
;
}
</span>
<div class="dropdown-menu pull-right">
@if (invoice.CanMarkInvalid)
{
<button class="dropdown-item small cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="invalid">
Mark as invalid <span class="fa fa-times"></span>
</button>
}
@if (invoice.CanMarkSettled)
{
<button class="dropdown-item small cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="settled">
Mark as settled <span class="fa fa-check-circle"></span>
</button>
}
</div>
</div>
}
else
{
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString().ToLower() *@
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
{
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString())
;
}
</span>
}
@foreach (var paymentType in invoice.Details.Payments.Select(payment => payment.GetPaymentMethodId()?.PaymentType).Distinct().Where(type => type != null && !string.IsNullOrEmpty(type.GetBadge())))
{
<span class="badge">@paymentType.GetBadge()</span>
}
</td>
<td class="text-end text-nowrap">@invoice.AmountCurrency</td>
<td class="text-end text-nowrap">
@if (invoice.ShowCheckout)
{
<span>
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId" class="invoice-checkout-link" id="invoice-checkout-@invoice.InvoiceId">Checkout</a>
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId" class="showInvoice only-for-js" data-invoice-id="@invoice.InvoiceId">[^]</a>
@if (!invoice.CanMarkStatus)
{
<span>-</span>
}
</span>
}
&nbsp;
<a asp-action="Invoice" class="invoice-details-link" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
<a class="only-for-js invoice-details-toggle" href="#">
<span title="Invoice Details Toggle" class="fa fa-1x fa-angle-double-down"></span>
</a>
</td>
</tr>
<tr id="invoice_details_@invoice.InvoiceId" class="invoice-details-row" style="display:none;">
<td colspan="99" class="border-top-0">
<div style="margin-left: 15px; margin-bottom: 0;">
@* Leaving this as partial because it abstracts complexity of Invoice Payments *@
<partial name="ListInvoicesPaymentsPartial" model="(invoice.Details, true)" />
</div>
</div>
}
else
{
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString().ToLower() *@
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
{
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString())
;
}
</span>
}
@foreach (var paymentType in invoice.Details.Payments.Select(payment => payment.GetPaymentMethodId()?.PaymentType).Distinct().Where(type => type != null && !string.IsNullOrEmpty(type.GetBadge())))
{
<span class="badge">@paymentType.GetBadge()</span>
}
</td>
<td style="text-align:right">@invoice.AmountCurrency</td>
<td style="text-align:right">
@if (invoice.ShowCheckout)
{
<span>
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId" class="invoice-checkout-link" id="invoice-checkout-@invoice.InvoiceId">Checkout</a>
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId" class="showInvoice only-for-js" data-invoice-id="@invoice.InvoiceId">[^]</a>
@if (!invoice.CanMarkStatus)
{
<span>-</span>
}
</span>
}
&nbsp;
<a asp-action="Invoice" class="invoice-details-link" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
<a class="only-for-js invoice-details-toggle" href="#">
<span title="Invoice Details Toggle" class="fa fa-1x fa-angle-double-down"></span>
</a>
</td>
</tr>
<tr id="invoice_details_@invoice.InvoiceId" class="invoice-details-row" style="display:none;">
<td colspan="99" class="border-top-0">
<div style="margin-left: 15px; margin-bottom: 0;">
@* Leaving this as partial because it abstracts complexity of Invoice Payments *@
<partial name="ListInvoicesPaymentsPartial" model="(invoice.Details, true)"/>
</div>
</td>
</tr>
}
</tbody>
</table>
</td>
</tr>
}
</tbody>
</table>
</div>
<vc:pager view-model="Model"/>
</form>
}
else
{
<p class="text-secondary mt-3">
There are no invoices matching your criteria.
</p>
}
<vc:pager view-model="Model" />
}
else
{
<p class="text-secondary mt-3">
There are no invoices matching your criteria.
</p>
}
</form>