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
8 changed files with 362 additions and 335 deletions

View File

@@ -393,6 +393,7 @@ namespace BTCPayServer.Tests
var storeUrl = s.Driver.Url; var storeUrl = s.Driver.Url;
s.ClickOnAllSectionLinks(); s.ClickOnAllSectionLinks();
s.GoToInvoices(); s.GoToInvoices();
Assert.Contains("There are no invoices matching your criteria.", s.Driver.PageSource);
var invoiceId = s.CreateInvoice(); var invoiceId = s.CreateInvoice();
s.FindAlertMessage(); s.FindAlertMessage();
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click(); s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
@@ -415,6 +416,23 @@ namespace BTCPayServer.Tests
s.GoToInvoices(); s.GoToInvoices();
Assert.Contains(invoiceId, s.Driver.PageSource); 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 // When logout out we should not be able to access store and invoice details
s.Logout(); s.Logout();
s.Driver.Navigate().GoToUrl(storeUrl); s.Driver.Navigate().GoToUrl(storeUrl);

View File

@@ -437,8 +437,12 @@ namespace BTCPayServer.Controllers
{ {
case "archive": case "archive":
await _InvoiceRepository.MassArchive(selectedItems); 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; break;
} }
} }
@@ -763,6 +767,8 @@ namespace BTCPayServer.Controllers
invoiceQuery.Skip = model.Skip; invoiceQuery.Skip = model.Skip;
var list = await _InvoiceRepository.GetInvoices(invoiceQuery); var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
model.IncludeArchived = invoiceQuery.IncludeArchived;
foreach (var invoice in list) foreach (var invoice in list)
{ {
var state = invoice.GetInvoiceState(); var state = invoice.GetInvoiceState();

View File

@@ -10,6 +10,7 @@ namespace BTCPayServer.Models.InvoicingModels
public List<InvoiceModel> Invoices { get; set; } = new List<InvoiceModel>(); public List<InvoiceModel> Invoices { get; set; } = new List<InvoiceModel>();
public string[] StoreIds { get; set; } public string[] StoreIds { get; set; }
public string StoreId { get; set; } public string StoreId { get; set; }
public bool IncludeArchived { get; set; }
} }
public class InvoiceModel 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()) using (var context = _applicationDbContextFactory.CreateContext())
{ {
@@ -458,7 +458,7 @@ namespace BTCPayServer.Services.Invoices
foreach (InvoiceData invoice in items) foreach (InvoiceData invoice in items)
{ {
invoice.Archived = true; invoice.Archived = archive;
} }
await context.SaveChangesAsync(); await context.SaveChangesAsync();

View File

@@ -47,108 +47,109 @@
<h4 class="mt-5 mb-4">Invoice Details</h4> <h4 class="mt-5 mb-4">Invoice Details</h4>
} }
<div class="form-group"> <div class="d-flex justify-content-between">
<label asp-for="Amount" class="form-label"></label> <div class="form-group flex-fill me-4">
<input asp-for="Amount" class="form-control" /> <label asp-for="Amount" class="form-label"></label>
<span asp-validation-for="Amount" class="text-danger"></span> <input asp-for="Amount" class="form-control" />
</div> <span asp-validation-for="Amount" class="text-danger"></span>
<div class="form-group"> </div>
<label asp-for="Currency" class="form-label"></label> <div class="form-group">
<input asp-for="Currency" class="form-control" /> <label asp-for="Currency" class="form-label"></label>
<span asp-validation-for="Currency" class="text-danger"></span> <input asp-for="Currency" class="form-control" />
</div> <span asp-validation-for="Currency" class="text-danger"></span>
<div class="form-group"> </div>
<label asp-for="OrderId" class="form-label"></label> </div>
<input asp-for="OrderId" class="form-control" /> <div class="form-group">
<span asp-validation-for="OrderId" class="text-danger"></span> <label asp-for="OrderId" class="form-label"></label>
</div> <input asp-for="OrderId" class="form-control" />
<div class="form-group"> <span asp-validation-for="OrderId" class="text-danger"></span>
<label asp-for="ItemDesc" class="form-label"></label> </div>
<input asp-for="ItemDesc" class="form-control" /> <div class="form-group">
<span asp-validation-for="ItemDesc" class="text-danger"></span> <label asp-for="ItemDesc" class="form-label"></label>
</div> <input asp-for="ItemDesc" class="form-control" />
<div class="form-group mb-4"> <span asp-validation-for="ItemDesc" class="text-danger"></span>
<label asp-for="SupportedTransactionCurrencies" class="form-label"></label> </div>
@foreach (var item in Model.AvailablePaymentMethods) <div class="form-group mb-4">
{ <label asp-for="SupportedTransactionCurrencies" class="form-label"></label>
<div class="form-check mb-2"> @foreach (var item in Model.AvailablePaymentMethods)
<label class="form-check-label"> {
<input name="SupportedTransactionCurrencies" class="form-check-input" checked="checked" type="checkbox" value="@item.Value"> <div class="form-check mb-2">
@item.Text <label class="form-check-label">
</label> <input name="SupportedTransactionCurrencies" class="form-check-input" checked="checked" type="checkbox" value="@item.Value">
</div> @item.Text
} </label>
<span asp-validation-for="SupportedTransactionCurrencies" class="text-danger"></span> </div>
</div> }
<div class="form-group"> <span asp-validation-for="SupportedTransactionCurrencies" class="text-danger"></span>
<label asp-for="DefaultPaymentMethod" class="form-label"></label> </div>
<select asp-for="DefaultPaymentMethod" asp-items="Model.AvailablePaymentMethods" class="form-select"> <div class="form-group">
<option value="" selected>Use the stores default</option> <label asp-for="DefaultPaymentMethod" class="form-label"></label>
</select> <select asp-for="DefaultPaymentMethod" asp-items="Model.AvailablePaymentMethods" class="form-select">
<span asp-validation-for="DefaultPaymentMethod" class="text-danger"></span> <option value="" selected>Use the stores default</option>
</div> </select>
<h4 class="mt-5 mb-4">Customer Information</h4> <span asp-validation-for="DefaultPaymentMethod" class="text-danger"></span>
<div class="form-group"> </div>
<label asp-for="BuyerEmail" class="form-label"></label> <h4 class="mt-5 mb-4">Customer Information</h4>
<input asp-for="BuyerEmail" class="form-control" /> <div class="form-group">
<span asp-validation-for="BuyerEmail" class="text-danger"></span> <label asp-for="BuyerEmail" class="form-label"></label>
</div> <input asp-for="BuyerEmail" class="form-control" />
<div class="form-group"> <span asp-validation-for="BuyerEmail" class="text-danger"></span>
<label asp-for="RequiresRefundEmail" class="form-label"></label> </div>
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select"></select> <div class="form-group">
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span> <label asp-for="RequiresRefundEmail" class="form-label"></label>
</div> <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> <h4 class="mt-5 mb-2">Additional Options</h4>
<div class="form-group"> <div class="form-group">
<div class="accordion" id="additional"> <div class="accordion" id="additional">
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="additional-pos-data-header"> <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"> <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 Point Of Sale Data
<vc:icon symbol="caret-down" /> <vc:icon symbol="caret-down" />
</button> </button>
</h2> </h2>
<div id="additional-pos-data" class="accordion-collapse collapse" aria-labelledby="additional-pos-data-header"> <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> <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"> <div class="form-group">
<label asp-for="PosData" class="form-label"></label> <label asp-for="PosData" class="form-label"></label>
<input asp-for="PosData" class="form-control"/> <input asp-for="PosData" class="form-control" />
<span asp-validation-for="PosData" class="text-danger"></span> <span asp-validation-for="PosData" class="text-danger"></span>
</div>
</div> </div>
</div> </div>
</div> <div class="accordion-item">
<div class="accordion-item"> <h2 class="accordion-header" id="additional-notifications-header">
<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">
<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
Invoice Notifications <vc:icon symbol="caret-down" />
<vc:icon symbol="caret-down"/> </button>
</button> </h2>
</h2> <div id="additional-notifications" class="accordion-collapse collapse" aria-labelledby="additional-notifications-header">
<div id="additional-notifications" class="accordion-collapse collapse" aria-labelledby="additional-notifications-header"> <div class="accordion-body">
<div class="accordion-body"> <div class="form-group">
<div class="form-group"> <label asp-for="NotificationUrl" class="form-label"></label>
<label asp-for="NotificationUrl" class="form-label"></label> <input asp-for="NotificationUrl" class="form-control" />
<input asp-for="NotificationUrl" class="form-control"/> <span asp-validation-for="NotificationUrl" class="text-danger"></span>
<span asp-validation-for="NotificationUrl" class="text-danger"></span> </div>
</div> <div class="form-group">
<div class="form-group"> <label asp-for="NotificationEmail" class="form-label"></label>
<label asp-for="NotificationEmail" class="form-label"></label> <input asp-for="NotificationEmail" class="form-control" />
<input asp-for="NotificationEmail" class="form-control"/> <span asp-validation-for="NotificationEmail" class="text-danger"></span>
<span asp-validation-for="NotificationEmail" class="text-danger"></span> <p id="InvoiceEmailHelpBlock" class="form-text text-muted">
<p id="InvoiceEmailHelpBlock" class="form-text text-muted"> Receive updates for this invoice.
Receive updates for this invoice. </p>
</p> </div>
</div> </div>
</div> </div>
</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 class="form-group mt-4"> </div>
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
</div>
</form> </form>
</div> </div>
</div> </div>

View File

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

View File

@@ -11,8 +11,8 @@
<h5 class="alert-heading">Updated in v1.4.0</h5> <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> <p class="mb-2">Invoice states have been updated to match the Greenfield API:</p>
<div class="row"> <div class="row">
<div class="col col-12 col-sm-6"> <div class="col-12 col-md-6">
<ul class="list-unstyled mb-sm-0"> <ul class="list-unstyled mb-md-0">
<li> <li>
<span class="badge badge-processing">Paid</span> <span class="badge badge-processing">Paid</span>
<span class="mx-1">is now shown as</span> <span class="mx-1">is now shown as</span>
@@ -30,7 +30,7 @@
</li> </li>
</ul> </ul>
</div> </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> <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>
</div> </div>

View File

@@ -6,7 +6,7 @@
} }
@section PageHeadContent { @section PageHeadContent {
<style type="text/css"> <style>
.invoice-payments { .invoice-payments {
padding-left: 2rem; padding-left: 2rem;
} }
@@ -16,14 +16,6 @@
font-weight: bold; font-weight: bold;
} }
.wraptext200 {
max-width: 200px;
text-overflow: ellipsis;
overflow: hidden;
display: block;
white-space: nowrap;
}
.pavpill { .pavpill {
display: inline-block; display: inline-block;
padding: 0.3em 0.5em; padding: 0.3em 0.5em;
@@ -69,6 +61,13 @@
background: #329f80; background: #329f80;
color: #fff; color: #fff;
} }
/* pull mass action form up, so that it is besides the search form */
@@media (min-width: 992px) {
#MassAction {
margin-top: -4rem;
}
}
</style> </style>
} }
@@ -150,7 +149,7 @@
}); });
$("#invoices") $("#invoices")
.on("click", ".invoice-row .invoice-details-toggle", function(e) { .on("click", ".invoice-row .invoice-details-toggle", function (e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(true); e.stopPropagation(true);
@@ -167,7 +166,7 @@
} }
}); });
}) })
.on("click", ".invoice-row", function(e) { .on("click", ".invoice-row", function (e) {
const $invoiceRow = $(e.currentTarget); const $invoiceRow = $(e.currentTarget);
if (!$(e.target).is("a,.badge,.selector")) { if (!$(e.target).is("a,.badge,.selector")) {
$invoiceRow.find(".selector").trigger("click"); $invoiceRow.find(".selector").trigger("click");
@@ -196,94 +195,55 @@
</a> </a>
</div> </div>
<partial name="InvoiceStatusChangePartial"/> <partial name="InvoiceStatusChangePartial" />
<div class="row"> @* Custom Range Modal *@
<div class="col-12 col-lg-6 mb-5 mb-lg-2 ms-auto"> <div class="modal fade" id="customRangeModal" tabindex="-1" role="dialog" aria-labelledby="customRangeModalTitle" aria-hidden="true" data-bs-backdrop="static">
<form asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" method="get"> <div class="modal-dialog modal-dialog-centered" role="document" style="max-width: 550px;">
<input type="hidden" asp-for="Count"/> <div class="modal-content">
<input asp-for="TimezoneOffset" type="hidden"/> <div class="modal-header">
<div class="input-group"> <h5 class="modal-title" id="customRangeModalTitle">Filter invoices by Custom Range</h5>
<a href="#help" class="input-group-text text-secondary text-decoration-none" data-bs-toggle="collapse"> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="fa fa-filter"></span> <vc:icon symbol="close" />
</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>
<button type="button" id="SearchOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> </div>
<span class="visually-hidden">Toggle Dropdown</span> <div class="modal-body">
</button> <div class="form-group row">
<label for="dtpStartDate" class="col-sm-3 col-form-label">Start Date</label>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="SearchOptionsToggle"> <div class="col-sm-9">
<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> <div class="input-group">
<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> <input id="dtpStartDate" class="form-control flatdtpicker" type="datetime-local"
<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> data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
<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> placeholder="Start Date" />
<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> <button type="button" class="btn btn-primary input-group-clear" title="Clear">
<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> <span class="fa fa-times"></span>
<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> </button>
<div role="separator" class="dropdown-divider"></div> </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> </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:-3d@{@storeIds}">Last 3 days</a> </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:-7d@{@storeIds}">Last 7 days</a> <div class="form-group row">
<button type="button" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#customRangeModal">Custom Range</button> <label class="col-sm-3 col-form-label">End Date</label>
<div role="separator" class="dropdown-divider"></div> <div class="col-sm-9">
<a class="dropdown-item" href="?searchTerm=">Unfiltered</a> <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> </div>
<span asp-validation-for="SearchTerm" class="text-danger"></span> <div class="modal-footer">
</form> <button id="btnCustomRangeDate" type="button" class="btn btn-primary">Filter</button>
@* 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> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row collapse" id="help"> <div id="help" class="row collapse">
<div class="col @(Model.Total > 0 ? "pt-3 pb-lg-5" : "")"> <div class="col-xl-8 pb-3">
<p> <p>
You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information. 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 /> Be sure to split your search parameters with comma, for example:<br />
@@ -304,17 +264,57 @@
</ul> </ul>
</div> </div>
</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) <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>
<form method="post" id="MassAction" asp-action="MassAction" class="mt-lg-n5"> <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" /> <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"> <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"> <button class="btn btn-secondary dropdown-toggle mb-1" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions Actions
</button> </button>
<div class="dropdown-menu" aria-labelledby="ActionsDropdownToggle"> <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> </div>
</span> </span>
<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> <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> </div>
</span> </span>
</div>
<a href="https://docs.btcpayserver.org/Accounting/" class="ms-1" target="_blank" rel="noreferrer noopener"> @if (Model.Total > 0)
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span> {
</a> <div style="clear:both"></div>
<div class="table-responsive">
<table id="invoices" class="table table-hover table-responsive-md mt-4"> <table id="invoices" class="table table-hover">
<thead> <thead>
<tr> <tr>
<th style="width:2rem;" class="only-for-js"> <th style="width:2rem;" class="only-for-js">
<input id="selectAllCheckbox" type="checkbox" class="form-check-input"/> <input id="selectAllCheckbox" type="checkbox" class="form-check-input" />
</th> <th style="min-width:90px;" class="col-md-auto">
<th style="min-width:90px;" class="col-md-auto"> Date
Date <a id="switchTimeFormat" href="#">
<a id="switchTimeFormat" href="#"> <span class="fa fa-clock-o" title="Switch date format"></span>
<span class="fa fa-clock-o" title="Switch date format"></span> </a>
</a> </th>
</th> <th class="text-nowrap">Order Id</th>
<th style="max-width: 180px;">OrderId</th> <th class="text-nowrap">Invoice Id</th>
<th>InvoiceId</th> <th>Status</th>
<th style="min-width: 150px;">Status</th> <th class="text-end">Amount</th>
<th style="text-align:right">Amount</th> <th class="text-end">Actions</th>
<th style="text-align:right">Actions</th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> @foreach (var invoice in Model.Invoices)
@foreach (var invoice in Model.Invoices) {
{ <tr id="invoice_@invoice.InvoiceId" class="invoice-row">
<tr id="invoice_@invoice.InvoiceId" class="invoice-row"> <td class="only-for-js">
<td class="only-for-js"> <input name="selectedItems" type="checkbox" class="selector form-check-input" value="@invoice.InvoiceId" />
<input name="selectedItems" type="checkbox" class="selector form-check-input" value="@invoice.InvoiceId"/> </td>
</td> <td>
<td> <span class="switchTimeFormat" data-switch="@invoice.Date.ToTimeAgo()">
<span class="switchTimeFormat" data-switch="@invoice.Date.ToTimeAgo()"> @invoice.Date.ToBrowserDate()
@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())
;
}
</span> </span>
<div class="dropdown-menu pull-right"> </td>
@if (invoice.CanMarkInvalid) <td style="max-width:120px;">
{ @if (invoice.RedirectUrl != string.Empty)
<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> <a href="@invoice.RedirectUrl" class="wraptextAuto" rel="noreferrer noopener">@invoice.OrderId</a>
</button> }
} else
@if (invoice.CanMarkSettled) {
{ <span>@invoice.OrderId</span>
<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> </td>
</button> <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>
</div> </td>
} </tr>
else }
{ </tbody>
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()"> </table>
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString().ToLower() *@ </div>
@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>
<vc:pager view-model="Model"/> <vc:pager view-model="Model" />
</form> }
} else
else {
{ <p class="text-secondary mt-3">
<p class="text-secondary mt-3"> There are no invoices matching your criteria.
There are no invoices matching your criteria. </p>
</p> }
} </form>