Improve invoices list view (#1815)

* Improve invoices list view

* Pager: Only show options that make sense

* ListInvoices formatting

* Add separator for dropdown toggle split

* Minor ListInvoices improvement

* Improve payment requests list view

* Distinguish empty and filtered lists

* Properly align invoice details

* Add payment symbols to invoices list

* Improve payment symbols in invoices list

* Always display search on list pages

* Inline variable

* Move display logic to pager

e5040ede55 (commitcomment-41698272)
This commit is contained in:
Dennis Reimann 2020-08-24 06:57:07 +02:00 committed by GitHub
parent a249a164f7
commit e7ea8ac40f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 428 additions and 362 deletions

View file

@ -1,8 +1,8 @@
@model BasePagingViewModel @model BasePagingViewModel
<nav aria-label="..." class="w-100"> @if (Model.Total > 0)
@if (Model.Total != 0) {
{ <nav aria-label="..." class="w-100">
<ul class="pagination float-left"> <ul class="pagination float-left">
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)"> <li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)">&laquo;</a> <a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)">&laquo;</a>
@ -25,25 +25,38 @@
<a class="page-link" href="@NavigatePages(1, Model.Count)">&raquo;</a> <a class="page-link" href="@NavigatePages(1, Model.Count)">&raquo;</a>
</li> </li>
</ul> </ul>
}
<ul class="pagination float-right"> @if (Model.Total >= 50)
<li class="page-item disabled"> {
<span class="page-link">Page Size:</span> <ul class="pagination float-right">
</li> <li class="page-item disabled">
<li class="page-item @(Model.Count == 50 ? "active" : null)"> <span class="page-link">Page Size:</span>
<a class="page-link" href="@NavigatePages(0, 50)">50</a> </li>
</li> <li class="page-item @(Model.Count == 50 ? "active" : null)">
<li class="page-item @(Model.Count == 100 ? "active" : null)"> <a class="page-link" href="@NavigatePages(0, 50)">50</a>
<a class="page-link" href="@NavigatePages(0, 100)">100</a> </li>
</li> @if (Model.Total >= 100)
<li class="page-item @(Model.Count == 250 ? "active" : null)"> {
<a class="page-link" href="@NavigatePages(0, 250)">250</a> <li class="page-item @(Model.Count == 100 ? "active" : null)">
</li> <a class="page-link" href="@NavigatePages(0, 100)">100</a>
<li class="page-item @(Model.Count == 500 ? "active" : null)"> </li>
<a class="page-link" href="@NavigatePages(0, 500)">500</a> }
</li> @if (Model.Total >= 250)
</ul> {
</nav> <li class="page-item @(Model.Count == 250 ? "active" : null)">
<a class="page-link" href="@NavigatePages(0, 250)">250</a>
</li>
}
@if (Model.Total >= 500)
{
<li class="page-item @(Model.Count == 500 ? "active" : null)">
<a class="page-link" href="@NavigatePages(0, 500)">500</a>
</li>
}
</ul>
}
</nav>
}
@{ @{
string NavigatePages(int prevNext, int count) string NavigatePages(int prevNext, int count)
{ {

View file

@ -623,7 +623,6 @@ namespace BTCPayServer.Controllers
model.Invoices.Add(new InvoiceModel() model.Invoices.Add(new InvoiceModel()
{ {
Status = invoice.Status, Status = invoice.Status,
StatusString = state.ToString(),
ShowCheckout = invoice.Status == InvoiceStatus.New, ShowCheckout = invoice.Status == InvoiceStatus.New,
Date = invoice.InvoiceTime, Date = invoice.InvoiceTime,
InvoiceId = invoice.Id, InvoiceId = invoice.Id,

View file

@ -19,7 +19,6 @@ namespace BTCPayServer.Models.InvoicingModels
public string InvoiceId { get; set; } public string InvoiceId { get; set; }
public InvoiceStatus Status { get; set; } public InvoiceStatus Status { get; set; }
public string StatusString { get; set; }
public bool CanMarkComplete { get; set; } public bool CanMarkComplete { get; set; }
public bool CanMarkInvalid { get; set; } public bool CanMarkInvalid { get; set; }
public bool CanMarkStatus => CanMarkComplete || CanMarkInvalid; public bool CanMarkStatus => CanMarkComplete || CanMarkInvalid;

View file

@ -1,322 +1,351 @@
@using BTCPayServer.Payments
@model InvoicesModel @model InvoicesModel
@{ @{
ViewData["Title"] = "Invoices"; ViewData["Title"] = "Invoices";
var storeIds = string.Join("", Model.StoreIds.Select(storeId => $",storeid:{storeId}"));
} }
@section HeadScripts { @section HeadScripts {
<script src="~/modal/btcpay.js" asp-append-version="true"></script> <script src="~/modal/btcpay.js" asp-append-version="true"></script>
} }
@Html.HiddenFor(a => a.Count) @Html.HiddenFor(a => a.Count)
<section> <section>
<div class="container"> <div class="container">
@if (TempData.HasStatusMessage()) @if (TempData.HasStatusMessage())
{ {
<div class="row"> <div class="row">
<div class="col-lg-12 text-center"> <div class="col-lg-12 text-center">
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
</div>
</div> </div>
</div>
} }
<div class="row"> <div class="row">
<div class="col-lg-12 section-heading"> <div class="col-lg-12 section-heading">
<h2>@ViewData["Title"]</h2> <h2>@ViewData["Title"]</h2>
<hr class="primary"> <hr class="primary">
<p>Create, search or pay an invoice. (<a href="#help" data-toggle="collapse">Help</a>)</p> <p>Create, search or pay an invoice.</p>
<div id="help" class="collapse text-left">
<p>
You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.<br />
Be sure to split your search parameters with comma, for example: <code>startdate:2019-04-25 13:00:00, status:paid</code><br />
You can also apply filters to your search by searching for <code>filtername:value</code>, here is a list of supported filters
</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>
<p>
If you want all confirmed and complete invoices, you can duplicate a filter <code>status:confirmed, status:complete</code>.
</p>
</div>
</div> </div>
</div> </div>
<form asp-action="ListInvoices" method="get" class="pull-right mb-3 col-sm-12 col-md-7 col-lg-6"> <div class="row">
<input type="hidden" asp-for="Count" /> <div class="col-12 col-sm-4 col-lg-6 mb-3">
<div class="input-group "> <a asp-action="CreateInvoice" class="btn btn-primary mb-1" role="button" id="CreateNewInvoice">
<input asp-for="TimezoneOffset" type="hidden" /> <span class="fa fa-plus"></span>
<input asp-for="SearchTerm" class="form-control" /> Create an invoice
<div class="input-group-append"> </a>
<button type="submit" class="btn btn-primary" title="Search invoice">
<span class="fa fa-search"></span> Search
</button>
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
@{
var storeIds = string.Join(
"",
Model.StoreIds.Select(storeId => $",storeid:{storeId}")
);
}
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="/invoices?Count=@Model.Count&SearchTerm=status%3Ainvalid@{@storeIds}">Invalid Invoices</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&SearchTerm=status%3Apaid%2Cstatus%3Aconfirmed%2Cstatus%3Acomplete@{@storeIds}">Paid Invoices</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&SearchTerm=exceptionstatus%3ApaidLate@{@storeIds}s">Paid Late Invoices</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&SearchTerm=exceptionstatus%3ApaidPartial@{@storeIds}">Paid Partial Invoices</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&SearchTerm=exceptionstatus%3ApaidOver@{@storeIds}">Paid Over Invoices</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&SearchTerm=unusual%3Atrue@{@storeIds}">Unusual Invoices</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&SearchTerm=includearchived%3Atrue@{@storeIds}">Archived Invoices</a>
<div role="separator" class="dropdown-divider"></div>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&timezoneoffset=0&SearchTerm=startDate%3A-24h@{@storeIds}">Last 24 hours</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&timezoneoffset=0&SearchTerm=startDate%3A-3d@{@storeIds}">Last 3 days</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&timezoneoffset=0&SearchTerm=startDate%3A-7d@{@storeIds}">Last 7 days</a>
<button type="button" class="dropdown-item" data-toggle="modal" data-target="#customRangeModal" data-backdrop="static">Custom Range</button>
<div role="separator" class="dropdown-divider"></div>
<a class="dropdown-item" href="/invoices?SearchTerm=">Unfiltered</a>
</div>
</div>
</div> </div>
<span asp-validation-for="SearchTerm" class="text-danger"></span> <div class="col-12 col-sm-8 col-lg-6 mb-3">
</form> <form asp-action="ListInvoices" method="get">
<input type="hidden" asp-for="Count"/>
<form method="post" id="MassAction" asp-action="MassAction"> <input asp-for="TimezoneOffset" type="hidden"/>
<div class="row button-row col-sm-12 col-md-5 col-lg-6"> <div class="input-group">
<div> <div class="input-group-prepend">
<a asp-action="CreateInvoice" class="btn btn-primary mb-1" role="button" id="CreateNewInvoice"><span class="fa fa-plus"></span> Create a new invoice</a> <a href="#help" class="input-group-text text-secondary text-decoration-none" data-toggle="collapse">
<span class="fa fa-filter"></span>
<span> </a>
<button class="btn btn-primary dropdown-toggle mb-1" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
</button>
<div class="dropdown-menu">
<button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="archive"><i class="fa fa-archive"></i> Archive</button>
</div> </div>
</span> <input asp-for="SearchTerm" class="form-control"/>
<div class="input-group-append">
<span> <button type="submit" class="btn btn-secondary" title="Search invoice">
<a class="btn btn-primary dropdown-toggle mb-1" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <span class="fa fa-search"></span> Search
Export
</a>
<a href="https://docs.btcpayserver.org/Accounting/" target="_blank">
<span class="fa fa-question-circle-o" title="More information..."></span>
</a>
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="csv" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">CSV</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>
</span>
</div>
</div>
<br />
@* Custom Range Modal *@
<div class="modal fade" id="customRangeModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle" aria-hidden="true">
<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="exampleModalLongTitle">Filter invoices by Custom Range</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button> </button>
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="/invoices?Count=@Model.Count&SearchTerm=status%3Ainvalid@{@storeIds}">Invalid Invoices</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&SearchTerm=status%3Apaid%2Cstatus%3Aconfirmed%2Cstatus%3Acomplete@{@storeIds}">Paid Invoices</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&SearchTerm=exceptionstatus%3ApaidLate@{@storeIds}s">Paid Late Invoices</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&SearchTerm=exceptionstatus%3ApaidPartial@{@storeIds}">Paid Partial Invoices</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&SearchTerm=exceptionstatus%3ApaidOver@{@storeIds}">Paid Over Invoices</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&SearchTerm=unusual%3Atrue@{@storeIds}">Unusual Invoices</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&SearchTerm=includearchived%3Atrue@{@storeIds}">Archived Invoices</a>
<div role="separator" class="dropdown-divider"></div>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&timezoneoffset=0&SearchTerm=startDate%3A-24h@{@storeIds}">Last 24 hours</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&timezoneoffset=0&SearchTerm=startDate%3A-3d@{@storeIds}">Last 3 days</a>
<a class="dropdown-item" href="/invoices?Count=@Model.Count&timezoneoffset=0&SearchTerm=startDate%3A-7d@{@storeIds}">Last 7 days</a>
<button type="button" class="dropdown-item" data-toggle="modal" data-target="#customRangeModal" data-backdrop="static">Custom Range</button>
<div role="separator" class="dropdown-divider"></div>
<a class="dropdown-item" href="/invoices?SearchTerm=">Unfiltered</a>
</div>
</div> </div>
<div class="modal-body">
<div class="form-group row"> </div>
<label for="dtpStartDate" class="col-sm-3 col-form-label">Start Date</label> <span asp-validation-for="SearchTerm" class="text-danger"></span>
<div class="col-sm-9"> </form>
<div class="input-group">
<input id="dtpStartDate" class="form-control flatdtpicker" type="datetime-local" @* Custom Range Modal *@
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }' <div class="modal fade" id="customRangeModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle" aria-hidden="true">
placeholder="Start Date" /> <div class="modal-dialog modal-dialog-centered" role="document" style="max-width: 550px;">
<div class="input-group-append"> <div class="modal-content">
<button type="button" class="btn btn-primary input-group-clear" title="Clear"> <div class="modal-header">
<span class=" fa fa-times"></span> <h5 class="modal-title" id="exampleModalLongTitle">Filter invoices by Custom Range</h5>
</button> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</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" />
<div class="input-group-append">
<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="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" />
<div class="input-group-append">
<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> </div>
</div> </div>
<div class="form-group row"> <div class="modal-footer">
<label class="col-sm-3 col-form-label">End Date</label> <button id="btnCustomRangeDate" type="button" class="btn btn-primary">Filter</button>
<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" />
<div class="input-group-append">
<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> </div>
<div class="modal-footer">
<button id="btnCustomRangeDate" type="button" class="btn btn-primary">Filter</button>
</div>
</div> </div>
</div> </div>
</div> <script type="text/javascript">
<script type="text/javascript"> $('#btnCustomRangeDate').on('click', function (sender) {
$('#btnCustomRangeDate').on('click', function (sender) { var filterString = "";
var filterString = "";
var dtpStartDate = $("#dtpStartDate").val(); var dtpStartDate = $("#dtpStartDate").val();
if (dtpStartDate !== null && dtpStartDate !== "") { if (dtpStartDate !== null && dtpStartDate !== "") {
filterString = "startDate%3A" + dtpStartDate; filterString = "startDate%3A" + dtpStartDate;
}
var dtpEndDate = $("#dtpEndDate").val();
if (dtpEndDate !== null && dtpEndDate !== "") {
if (filterString !== "") {
filterString += ",";
} }
filterString += "endDate%3A" + dtpEndDate;
}
if (filterString !== "") { var dtpEndDate = $("#dtpEndDate").val();
var redirectUri = "/invoices?Count=" + $("#Count").val() + if (dtpEndDate !== null && dtpEndDate !== "") {
"&timezoneoffset=" + $("#TimezoneOffset").val() + if (filterString !== "") {
"&SearchTerm=" + filterString; filterString += ",";
}
filterString += "endDate%3A" + dtpEndDate;
}
window.location.href = redirectUri; if (filterString !== "") {
} else { var redirectUri = "/invoices?Count=" + $("#Count").val() +
$("#dtpStartDate").next().trigger("focus"); "&timezoneoffset=" + $("#TimezoneOffset").val() +
} "&SearchTerm=" + filterString;
})
</script>
@* Custom Range Modal *@
<script type="text/javascript"> window.location.href = redirectUri;
function selectAll(e) { } else {
var items = document.getElementsByClassName("selector"); $("#dtpStartDate").next().trigger("focus");
for (var i = 0; i < items.length; i++) { }
items[i].checked = e.checked; })
</script>
@* Custom Range Modal *@
</div>
</div>
<div class="row collapse" id="help">
<div class="col">
<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>
<p>
If you want all confirmed and complete invoices, you can duplicate a filter <code>status:confirmed, status:complete</code>.
</p>
</div>
</div>
@if (Model.Total > 0)
{
<form method="post" id="MassAction" asp-action="MassAction" class="mt-3">
<span class="mr-2">
<button class="btn btn-secondary dropdown-toggle mb-1" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
</button>
<div class="dropdown-menu">
<button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="archive"><i class="fa fa-archive"></i> Archive</button>
</div>
</span>
<span>
<a class="btn btn-secondary dropdown-toggle mb-1" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Export
</a>
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="csv" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">CSV</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>
</span>
<a href="https://docs.btcpayserver.org/Accounting/" class="ml-1" target="_blank">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<script type="text/javascript">
function selectAll(e)
{
var items = document.getElementsByClassName("selector");
for (var i = 0; i < items.length; i++) {
items[i].checked = e.checked;
}
} }
} </script>
</script>
<div class="row"> <table class="table table-sm table-responsive-md">
<div class="col-lg-12"> <thead>
<table class="table table-sm table-responsive-md"> <tr>
<thead> <th style="width:2rem;" class="only-for-js">
<tr> <input id="selectAllCheckbox" type="checkbox" onclick="selectAll(this);"/>
<th class="only-for-js"> </th>
@if (Model.Total > 0) <th style="min-width:90px;" class="col-md-auto">
{ Date
<input id="selectAllCheckbox" type="checkbox" onclick="selectAll(this);" /> <a href="javascript:switchTimeFormat()">
} <span class="fa fa-clock-o" title="Switch date format"></span>
</th> </a>
<th style="min-width: 90px;" class="col-md-auto"> </th>
Date <th style="max-width: 180px;">OrderId</th>
<a href="javascript:switchTimeFormat()"> <th>InvoiceId</th>
<span class="fa fa-clock-o" title="Switch date format"></span> <th style="min-width: 150px;">Status</th>
</a> <th style="text-align:right">Amount</th>
</th> <th style="text-align:right">Actions</th>
<th style="max-width: 180px;">OrderId</th> </tr>
<th>InvoiceId</th> </thead>
<th style="min-width: 150px;">Status</th> <tbody>
<th style="text-align:right">Amount</th> @foreach (var invoice in Model.Invoices)
<th style="text-align:right">Actions</th> {
</tr> <tr>
</thead> <td class="only-for-js">
<tbody> <input name="selectedItems" type="checkbox" class="selector" value="@invoice.InvoiceId"/>
@foreach (var invoice in Model.Invoices) </td>
{ <td>
<tr> <span class="switchTimeFormat" data-switch="@invoice.Date.ToTimeAgo()">
<td class="only-for-js"> @invoice.Date.ToBrowserDate()
<input name="selectedItems" type="checkbox" class="selector" value="@invoice.InvoiceId" /> </span>
</td> </td>
<td> <td style="max-width: 180px;">
<span class="switchTimeFormat" data-switch="@invoice.Date.ToTimeAgo()"> @if (invoice.RedirectUrl != string.Empty)
@invoice.Date.ToBrowserDate() {
</span>
</td>
<td style="max-width: 180px;">
@if (invoice.RedirectUrl != string.Empty)
{
<a href="@invoice.RedirectUrl" class="wraptext200">@invoice.OrderId</a> <a href="@invoice.RedirectUrl" class="wraptext200">@invoice.OrderId</a>
} }
else else
{ {
<span>@invoice.OrderId</span> <span>@invoice.OrderId</span>
} }
</td> </td>
<td>@invoice.InvoiceId</td> <td>@invoice.InvoiceId</td>
<td> <td>
@if(invoice.Details.Archived) @if (invoice.Details.Archived)
{ {
<span class="badge badge-warning">archived</span> <span class="badge badge-warning">archived</span>
} }
@if (invoice.CanMarkStatus) @if (invoice.CanMarkStatus)
{ {
<div id="pavpill_@invoice.InvoiceId"> <div id="pavpill_@invoice.InvoiceId" class="badge badge-@invoice.Status.ToString().ToLower()">
<span class="dropdown-toggle dropdown-toggle-split pavpill pavpil-@invoice.Status.ToString().ToLower()" <span class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> @invoice.Status.ToString().ToLower()
@invoice.StatusString
</span> </span>
<div class="dropdown-menu pull-right"> <div class="dropdown-menu pull-right">
@if (invoice.CanMarkInvalid) @if (invoice.CanMarkInvalid)
{ {
<button class="dropdown-item small cursorPointer" onclick="changeInvoiceState(this, '@invoice.InvoiceId', 'invalid')"> <button class="dropdown-item small cursorPointer" onclick="changeInvoiceState(this, '@invoice.InvoiceId', 'invalid')">
Mark as invalid <span class="fa fa-times"></span> Mark as invalid <span class="fa fa-times"></span>
</button> </button>
} }
@if (invoice.CanMarkComplete) @if (invoice.CanMarkComplete)
{ {
<button class="dropdown-item small cursorPointer" onclick="changeInvoiceState(this, '@invoice.InvoiceId', 'complete')"> <button class="dropdown-item small cursorPointer" onclick="changeInvoiceState(this, '@invoice.InvoiceId', 'complete')">
Mark as complete <span class="fa fa-check-circle"></span> Mark as complete <span class="fa fa-check-circle"></span>
</button> </button>
} }
</div> </div>
</div> </div>
} }
else else
{ {
<span class="pavpill pavpil-@invoice.Status.ToString().ToLower()">@invoice.StatusString</span> <span class="badge badge-@invoice.Status.ToString().ToLower()">
} @invoice.Status.ToString().ToLower()
</td> </span>
<td style="text-align:right">@invoice.AmountCurrency</td> }
<td style="text-align:right"> @{
@if (invoice.ShowCheckout) var grouped = invoice.Details.Payments.GroupBy(payment => payment.GetPaymentMethodId()?.PaymentType).Where(entities => entities.Key!= null);
{ var paiOnChain = grouped.Where(g => g.Key == BitcoinPaymentType.Instance).Any();
var paidOffChain = grouped.Where(g => g.Key == LightningPaymentType.Instance).Any();
}
@if (paiOnChain)
{
<span class="badge">🔗</span>
}
@if (paidOffChain)
{
<span class="badge">⚡️</span>
}
</td>
<td style="text-align:right">@invoice.AmountCurrency</td>
<td style="text-align:right">
@if (invoice.ShowCheckout)
{
<span> <span>
<a asp-action="Checkout" class="invoice-checkout-link" id="invoice-checkout-@invoice.InvoiceId" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> <a asp-action="Checkout" class="invoice-checkout-link" id="invoice-checkout-@invoice.InvoiceId" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a>
<a href="javascript:btcpay.showInvoice('@invoice.InvoiceId')">[^]</a> <a href="javascript:btcpay.showInvoice('@invoice.InvoiceId')">[^]</a>
@if (!invoice.CanMarkStatus) @if (!invoice.CanMarkStatus)
{ {
<span>-</span> <span>-</span>
}
</span>
} }
&nbsp; </span>
<a asp-action="Invoice" class="invoice-details-link" asp-route-invoiceId="@invoice.InvoiceId">Details</a> }
<a href="javascript:void(0);" onclick="detailsToggle(this, '@invoice.InvoiceId')"> &nbsp;
<span title="Invoice Details Toggle" class="fa fa-1x fa-angle-double-down"></span> <a asp-action="Invoice" class="invoice-details-link" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
</a> <a href="javascript:void(0);" onclick="detailsToggle(this, '@invoice.InvoiceId')">
</td> <span title="Invoice Details Toggle" class="fa fa-1x fa-angle-double-down"></span>
</tr> </a>
<tr id="invoice_@invoice.InvoiceId" style="display:none;"> </td>
<td colspan="99" class="border-top-0"> </tr>
<div style="margin-left: 15px; margin-bottom: 0;"> <tr id="invoice_@invoice.InvoiceId" style="display:none;">
@* Leaving this as partial because it abstracts complexity of Invoice Payments *@ <td colspan="99" class="border-top-0">
<partial name="ListInvoicesPaymentsPartial" model="(invoice.Details, true)" /> <div style="margin-left: 15px; margin-bottom: 0;">
</div> @* Leaving this as partial because it abstracts complexity of Invoice Payments *@
</td> <partial name="ListInvoicesPaymentsPartial" model="(invoice.Details, true)"/>
</tr> </div>
} </td>
</tbody> </tr>
</table> }
<vc:pager view-model="Model"></vc:pager> </tbody>
</div> </table>
</div>
</form> <vc:pager view-model="Model"></vc:pager>
</form>
}
else
{
<p class="text-secondary mt-3">
There are no invoices matching your criteria.
</p>
}
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
$(function () { $(function () {
@ -354,8 +383,8 @@
$.post("invoices/" + invoiceId + "/changestate/" + newState) $.post("invoices/" + invoiceId + "/changestate/" + newState)
.done(function (data) { .done(function (data) {
var statusHtml = "<span class='pavpill pavpil-" + newState + "'>" + data.statusString + " <span class='fa fa-check'></span></span>"; var statusHtml = "<span class='badge badge-" + newState + "'>" + data.statusString + " <span class='fa fa-check'></span></span>";
pavpill.html(statusHtml); pavpill.replaceWith(statusHtml);
}) })
.fail(function (data) { .fail(function (data) {
pavpill.html(originalHtml.replace("dropdown-menu pull-right show", "dropdown-menu pull-right")); pavpill.html(originalHtml.replace("dropdown-menu pull-right show", "dropdown-menu pull-right"));
@ -364,6 +393,9 @@
} }
</script> </script>
<style type="text/css"> <style type="text/css">
.invoice-payments {
padding-left: 2rem;
}
.invoice-payments h3 { .invoice-payments h3 {
font-size: 15px; font-size: 15px;
@ -390,35 +422,37 @@
border-radius: 0.25rem; border-radius: 0.25rem;
} }
.pavpill.dropdown-toggle { .badge .dropdown-toggle {
cursor: pointer; cursor: pointer;
} padding: 0;
}
.dropdown-item { .dropdown-item {
cursor: pointer; cursor: pointer;
} }
.pavpil-new { .badge-new {
background: #d4edda; background: #d4edda;
color: #000; color: #000;
} }
.pavpil-expired { .badge-expired {
background: #eee; background: #eee;
color: #000; color: #000;
} }
.pavpil-invalid { .badge-invalid {
background: #c94a47; background: #c94a47;
color: #fff; color: #fff;
} }
.pavpil-confirmed, .pavpil-paid { .badge-confirmed,
.badge-paid {
background: #f1c332; background: #f1c332;
color: #000; color: #000;
} }
.pavpil-complete { .badge-complete {
background: #329f80; background: #329f80;
color: #fff; color: #fff;
} }

View file

@ -18,80 +18,96 @@
<div class="col-lg-12 section-heading"> <div class="col-lg-12 section-heading">
<h2>Payment Requests</h2> <h2>Payment Requests</h2>
<hr class="primary"> <hr class="primary">
<p>Create, search or pay an payment request.</p> <p>
Create, search or pay a payment request.
<a href="https://docs.btcpayserver.org/PaymentRequests/" class="ml-1" target="_blank">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</p>
</div> </div>
</div> </div>
<form asp-action="GetPaymentRequests" method="get" class="pull-right"> <div class="row">
<input type="hidden" asp-for="Count" /> <div class="col-12 col-md-4 col-lg-6 mb-3">
<div class="input-group"> <a asp-action="EditPaymentRequest" class="btn btn-primary" role="button" id="CreatePaymentRequest">
<input asp-for="TimezoneOffset" type="hidden" /> <span class="fa fa-plus"></span>
<input asp-for="SearchTerm" class="form-control" style="width:300px;" /> Create a payment request
<div class="input-group-append">
<button type="submit" class="btn btn-primary" title="Search invoice">
<span class="fa fa-search"></span> Search
</button>
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="/payment-requests?Count=@Model.Count&SearchTerm=includearchived%3Atrue">Include Archived Payment Reqs</a>
<div role="separator" class="dropdown-divider"></div>
<a class="dropdown-item" href="/payment-requests?SearchTerm=">Unfiltered</a>
</div>
</div>
</div>
<span asp-validation-for="SearchTerm" class="text-danger"></span>
</form>
<div class="row button-row">
<div class="col-lg-12 pl-0">
<a asp-action="EditPaymentRequest" class="btn btn-primary" role="button" id="CreatePaymentRequest"><span class="fa fa-plus"></span> Create a new payment request</a>
<a href="https://docs.btcpayserver.org/PaymentRequests/" target="_blank">
<span class="fa fa-question-circle-o" title="More information..."></span>
</a> </a>
</div> </div>
<div class="col-12 col-md-8 col-lg-6 mb-3">
<form asp-action="GetPaymentRequests" method="get">
<input type="hidden" asp-for="Count"/>
<input type="hidden" asp-for="TimezoneOffset" />
<div class="input-group">
<input asp-for="SearchTerm" class="form-control" style="width:300px;"/>
<div class="input-group-append">
<button type="submit" class="btn btn-secondary" title="Search invoice">
<span class="fa fa-search"></span> Search
</button>
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="/payment-requests?Count=@Model.Count&SearchTerm=includearchived%3Atrue">Include Archived Payment Reqs</a>
<div role="separator" class="dropdown-divider"></div>
<a class="dropdown-item" href="/payment-requests?SearchTerm=">Unfiltered</a>
</div>
</div>
</div>
<span asp-validation-for="SearchTerm" class="text-danger"></span>
</form>
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<table class="table table-sm table-responsive-md"> @if (Model.Total > 0)
<thead> {
<tr> <table class="table table-sm table-responsive-md">
<th>Title</th> <thead>
<th>Expiry</th>
<th class="text-right">Price</th>
<th class="text-right">Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr> <tr>
<td>@item.Title</td> <th>Title</th>
<td>@(item.ExpiryDate?.ToString("g") ?? "No Expiry")</td> <th>Expiry</th>
<td class="text-right">@item.Amount @item.Currency</td> <th class="text-right">Price</th>
<td class="text-right">@item.Status</td> <th class="text-right">Status</th>
<td class="text-right"> <th class="text-right">Actions</th>
<a asp-action="EditPaymentRequest" asp-route-id="@item.Id">Edit</a>
<span> - </span>
<a asp-action="ViewPaymentRequest" asp-route-id="@item.Id">View</a>
<span> - </span>
<a target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(item.Id)}")">Invoices</a>
<span> - </span>
<a target="_blank" asp-action="PayPaymentRequest" asp-route-id="@item.Id">Pay</a>
<span> - </span>
<a target="_blank" asp-action="ClonePaymentRequest" asp-route-id="@item.Id">Clone</a>
<span> - </span>
<a asp-action="TogglePaymentRequestArchival" asp-route-id="@item.Id">@(item.Archived ? "Unarchive" : "Archive")</a>
</td>
</tr> </tr>
} </thead>
</tbody> <tbody>
</table> @foreach (var item in Model.Items)
{
<tr>
<td>@item.Title</td>
<td>@(item.ExpiryDate?.ToString("g") ?? "No Expiry")</td>
<td class="text-right">@item.Amount @item.Currency</td>
<td class="text-right">@item.Status</td>
<td class="text-right">
<a asp-action="EditPaymentRequest" asp-route-id="@item.Id">Edit</a>
<span> - </span>
<a asp-action="ViewPaymentRequest" asp-route-id="@item.Id">View</a>
<span> - </span>
<a target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(item.Id)}")">Invoices</a>
<span> - </span>
<a target="_blank" asp-action="PayPaymentRequest" asp-route-id="@item.Id">Pay</a>
<span> - </span>
<a target="_blank" asp-action="ClonePaymentRequest" asp-route-id="@item.Id">Clone</a>
<span> - </span>
<a asp-action="TogglePaymentRequestArchival" asp-route-id="@item.Id">@(item.Archived ? "Unarchive" : "Archive")</a>
</td>
</tr>
}
</tbody>
</table>
<vc:pager view-model="Model"></vc:pager> <vc:pager view-model="Model"></vc:pager>
}
else
{
<p class="text-secondary mt-3">
There are no payment requests matching your criteria.
</p>
}
</div> </div>
</div> </div>
</div> </div>

View file

@ -76,6 +76,11 @@ ul li {
opacity: .5; opacity: .5;
} }
.dropdown-toggle-split,
.dropdown-toggle-split:hover {
border-left: 1px solid var(--btcpay-color-white);
}
/* Admin Sidebar Navigation */ /* Admin Sidebar Navigation */
a.nav-link { a.nav-link {
color: var(--btcpay-nav-color-link, var(--btcpay-color-neutral-600)); color: var(--btcpay-nav-color-link, var(--btcpay-color-neutral-600));