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();
@ -415,6 +416,23 @@ namespace BTCPayServer.Tests
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();
s.Driver.Navigate().GoToUrl(storeUrl);

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

@ -47,7 +47,8 @@
<h4 class="mt-5 mb-4">Invoice Details</h4>
}
<div class="form-group">
<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>
@ -57,6 +58,7 @@
<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" />
@ -145,7 +147,6 @@
</div>
</div>
</div>
<div class="form-group mt-4">
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
</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;
@ -69,6 +61,13 @@
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>
}
@ -198,43 +197,6 @@
<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
</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>
<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;">
@ -279,11 +241,9 @@
</div>
</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,28 +326,28 @@
<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">
@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>
<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>
<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>
@ -362,17 +362,17 @@
@invoice.Date.ToBrowserDate()
</span>
</td>
<td style="max-width: 180px;">
<td style="max-width:120px;">
@if (invoice.RedirectUrl != string.Empty)
{
<a href="@invoice.RedirectUrl" class="wraptext200" rel="noreferrer noopener">@invoice.OrderId</a>
<a href="@invoice.RedirectUrl" class="wraptextAuto" rel="noreferrer noopener">@invoice.OrderId</a>
}
else
{
<span>@invoice.OrderId</span>
}
</td>
<td>@invoice.InvoiceId</td>
<td class="text-break">@invoice.InvoiceId</td>
<td>
@if (invoice.Details.Archived)
{
@ -421,8 +421,8 @@
<span class="badge">@paymentType.GetBadge()</span>
}
</td>
<td style="text-align:right">@invoice.AmountCurrency</td>
<td style="text-align:right">
<td class="text-end text-nowrap">@invoice.AmountCurrency</td>
<td class="text-end text-nowrap">
@if (invoice.ShowCheckout)
{
<span>
@ -452,9 +452,9 @@
}
</tbody>
</table>
</div>
<vc:pager view-model="Model" />
</form>
}
else
{
@ -462,3 +462,4 @@ else
There are no invoices matching your criteria.
</p>
}
</form>