mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 18:11:36 +01:00
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:
parent
3c5d809cf9
commit
4b941a5145
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -47,108 +47,109 @@
|
||||
|
||||
<h4 class="mt-5 mb-4">Invoice Details</h4>
|
||||
}
|
||||
<div class="form-group">
|
||||
<label asp-for="Amount" class="form-label"></label>
|
||||
<input asp-for="Amount" class="form-control" />
|
||||
<span asp-validation-for="Amount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Currency" class="form-label"></label>
|
||||
<input asp-for="Currency" class="form-control" />
|
||||
<span asp-validation-for="Currency" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="OrderId" class="form-label"></label>
|
||||
<input asp-for="OrderId" class="form-control" />
|
||||
<span asp-validation-for="OrderId" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ItemDesc" class="form-label"></label>
|
||||
<input asp-for="ItemDesc" class="form-control" />
|
||||
<span asp-validation-for="ItemDesc" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group mb-4">
|
||||
<label asp-for="SupportedTransactionCurrencies" class="form-label"></label>
|
||||
@foreach (var item in Model.AvailablePaymentMethods)
|
||||
{
|
||||
<div class="form-check mb-2">
|
||||
<label class="form-check-label">
|
||||
<input name="SupportedTransactionCurrencies" class="form-check-input" checked="checked" type="checkbox" value="@item.Value">
|
||||
@item.Text
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<span asp-validation-for="SupportedTransactionCurrencies" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DefaultPaymentMethod" class="form-label"></label>
|
||||
<select asp-for="DefaultPaymentMethod" asp-items="Model.AvailablePaymentMethods" class="form-select">
|
||||
<option value="" selected>Use the store’s default</option>
|
||||
</select>
|
||||
<span asp-validation-for="DefaultPaymentMethod" class="text-danger"></span>
|
||||
</div>
|
||||
<h4 class="mt-5 mb-4">Customer Information</h4>
|
||||
<div class="form-group">
|
||||
<label asp-for="BuyerEmail" class="form-label"></label>
|
||||
<input asp-for="BuyerEmail" class="form-control" />
|
||||
<span asp-validation-for="BuyerEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="RequiresRefundEmail" class="form-label"></label>
|
||||
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select"></select>
|
||||
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="form-group flex-fill me-4">
|
||||
<label asp-for="Amount" class="form-label"></label>
|
||||
<input asp-for="Amount" class="form-control" />
|
||||
<span asp-validation-for="Amount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Currency" class="form-label"></label>
|
||||
<input asp-for="Currency" class="form-control" />
|
||||
<span asp-validation-for="Currency" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="OrderId" class="form-label"></label>
|
||||
<input asp-for="OrderId" class="form-control" />
|
||||
<span asp-validation-for="OrderId" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ItemDesc" class="form-label"></label>
|
||||
<input asp-for="ItemDesc" class="form-control" />
|
||||
<span asp-validation-for="ItemDesc" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group mb-4">
|
||||
<label asp-for="SupportedTransactionCurrencies" class="form-label"></label>
|
||||
@foreach (var item in Model.AvailablePaymentMethods)
|
||||
{
|
||||
<div class="form-check mb-2">
|
||||
<label class="form-check-label">
|
||||
<input name="SupportedTransactionCurrencies" class="form-check-input" checked="checked" type="checkbox" value="@item.Value">
|
||||
@item.Text
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<span asp-validation-for="SupportedTransactionCurrencies" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DefaultPaymentMethod" class="form-label"></label>
|
||||
<select asp-for="DefaultPaymentMethod" asp-items="Model.AvailablePaymentMethods" class="form-select">
|
||||
<option value="" selected>Use the store’s default</option>
|
||||
</select>
|
||||
<span asp-validation-for="DefaultPaymentMethod" class="text-danger"></span>
|
||||
</div>
|
||||
<h4 class="mt-5 mb-4">Customer Information</h4>
|
||||
<div class="form-group">
|
||||
<label asp-for="BuyerEmail" class="form-label"></label>
|
||||
<input asp-for="BuyerEmail" class="form-control" />
|
||||
<span asp-validation-for="BuyerEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="RequiresRefundEmail" class="form-label"></label>
|
||||
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select"></select>
|
||||
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-5 mb-2">Additional Options</h4>
|
||||
<div class="form-group">
|
||||
<div class="accordion" id="additional">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="additional-pos-data-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-pos-data" aria-expanded="false" aria-controls="additional-pos-data">
|
||||
Point Of Sale Data
|
||||
<vc:icon symbol="caret-down" />
|
||||
</button>
|
||||
</h2>
|
||||
<div id="additional-pos-data" class="accordion-collapse collapse" aria-labelledby="additional-pos-data-header">
|
||||
<p>Custom data to correlate the invoice with an order. This data can be a simple text, number or JSON object, e.g. <code>{ "orderId": 615, "product": "Pizza" }</code></p>
|
||||
<div class="form-group">
|
||||
<label asp-for="PosData" class="form-label"></label>
|
||||
<input asp-for="PosData" class="form-control"/>
|
||||
<span asp-validation-for="PosData" class="text-danger"></span>
|
||||
<h4 class="mt-5 mb-2">Additional Options</h4>
|
||||
<div class="form-group">
|
||||
<div class="accordion" id="additional">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="additional-pos-data-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-pos-data" aria-expanded="false" aria-controls="additional-pos-data">
|
||||
Point Of Sale Data
|
||||
<vc:icon symbol="caret-down" />
|
||||
</button>
|
||||
</h2>
|
||||
<div id="additional-pos-data" class="accordion-collapse collapse" aria-labelledby="additional-pos-data-header">
|
||||
<p>Custom data to correlate the invoice with an order. This data can be a simple text, number or JSON object, e.g. <code>{ "orderId": 615, "product": "Pizza" }</code></p>
|
||||
<div class="form-group">
|
||||
<label asp-for="PosData" class="form-label"></label>
|
||||
<input asp-for="PosData" class="form-control" />
|
||||
<span asp-validation-for="PosData" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="additional-notifications-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-notifications" aria-expanded="false" aria-controls="additional-notifications">
|
||||
Invoice Notifications
|
||||
<vc:icon symbol="caret-down"/>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="additional-notifications" class="accordion-collapse collapse" aria-labelledby="additional-notifications-header">
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="NotificationUrl" class="form-label"></label>
|
||||
<input asp-for="NotificationUrl" class="form-control"/>
|
||||
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="NotificationEmail" class="form-label"></label>
|
||||
<input asp-for="NotificationEmail" class="form-control"/>
|
||||
<span asp-validation-for="NotificationEmail" class="text-danger"></span>
|
||||
<p id="InvoiceEmailHelpBlock" class="form-text text-muted">
|
||||
Receive updates for this invoice.
|
||||
</p>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="additional-notifications-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-notifications" aria-expanded="false" aria-controls="additional-notifications">
|
||||
Invoice Notifications
|
||||
<vc:icon symbol="caret-down" />
|
||||
</button>
|
||||
</h2>
|
||||
<div id="additional-notifications" class="accordion-collapse collapse" aria-labelledby="additional-notifications-header">
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="NotificationUrl" class="form-label"></label>
|
||||
<input asp-for="NotificationUrl" class="form-control" />
|
||||
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="NotificationEmail" class="form-label"></label>
|
||||
<input asp-for="NotificationEmail" class="form-control" />
|
||||
<span asp-validation-for="NotificationEmail" class="text-danger"></span>
|
||||
<p id="InvoiceEmailHelpBlock" class="form-text text-muted">
|
||||
Receive updates for this invoice.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mt-4">
|
||||
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
|
||||
</div>
|
||||
<div class="form-group mt-4">
|
||||
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
}
|
||||
|
||||
@ -150,7 +149,7 @@
|
||||
});
|
||||
|
||||
$("#invoices")
|
||||
.on("click", ".invoice-row .invoice-details-toggle", function(e) {
|
||||
.on("click", ".invoice-row .invoice-details-toggle", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(true);
|
||||
|
||||
@ -167,7 +166,7 @@
|
||||
}
|
||||
});
|
||||
})
|
||||
.on("click", ".invoice-row", function(e) {
|
||||
.on("click", ".invoice-row", function (e) {
|
||||
const $invoiceRow = $(e.currentTarget);
|
||||
if (!$(e.target).is("a,.badge,.selector")) {
|
||||
$invoiceRow.find(".selector").trigger("click");
|
||||
@ -196,94 +195,55 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<partial name="InvoiceStatusChangePartial"/>
|
||||
<partial name="InvoiceStatusChangePartial" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6 mb-5 mb-lg-2 ms-auto">
|
||||
<form asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" method="get">
|
||||
<input type="hidden" asp-for="Count"/>
|
||||
<input asp-for="TimezoneOffset" type="hidden"/>
|
||||
<div class="input-group">
|
||||
<a href="#help" class="input-group-text text-secondary text-decoration-none" data-bs-toggle="collapse">
|
||||
<span class="fa fa-filter"></span>
|
||||
</a>
|
||||
<input asp-for="SearchTerm" class="form-control"/>
|
||||
<button type="submit" class="btn btn-secondary" title="Search invoice">
|
||||
<span class="fa fa-search"></span> Search
|
||||
@* Custom Range Modal *@
|
||||
<div class="modal fade" id="customRangeModal" tabindex="-1" role="dialog" aria-labelledby="customRangeModalTitle" aria-hidden="true" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document" style="max-width: 550px;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="customRangeModalTitle">Filter invoices by Custom Range</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<vc:icon symbol="close" />
|
||||
</button>
|
||||
<button type="button" id="SearchOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="SearchOptionsToggle">
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="status:invalid@{@storeIds}">Invalid Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="status:processing,status:settled@{@storeIds}">Paid Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidLate@{@storeIds}">Paid Late Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidPartial@{@storeIds}">Paid Partial Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidOver@{@storeIds}">Paid Over Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="unusual:true@{@storeIds}">Unusual Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="includearchived:true@{@storeIds}">Archived Invoices</a>
|
||||
<div role="separator" class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-timezoneoffset="0" asp-route-searchTerm="startDate:-24h@{@storeIds}">Last 24 hours</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-timezoneoffset="0" asp-route-searchTerm="startDate:-3d@{@storeIds}">Last 3 days</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-timezoneoffset="0" asp-route-searchTerm="startDate:-7d@{@storeIds}">Last 7 days</a>
|
||||
<button type="button" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#customRangeModal">Custom Range</button>
|
||||
<div role="separator" class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="?searchTerm=">Unfiltered</a>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label for="dtpStartDate" class="col-sm-3 col-form-label">Start Date</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input id="dtpStartDate" class="form-control flatdtpicker" type="datetime-local"
|
||||
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
|
||||
placeholder="Start Date" />
|
||||
<button type="button" class="btn btn-primary input-group-clear" title="Clear">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">End Date</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input id="dtpEndDate" class="form-control flatdtpicker" type="datetime-local"
|
||||
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
|
||||
placeholder="End Date" />
|
||||
<button type="button" class="btn btn-primary input-group-clear" title="Clear">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span asp-validation-for="SearchTerm" class="text-danger"></span>
|
||||
</form>
|
||||
|
||||
@* Custom Range Modal *@
|
||||
<div class="modal fade" id="customRangeModal" tabindex="-1" role="dialog" aria-labelledby="customRangeModalTitle" aria-hidden="true" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document" style="max-width: 550px;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="customRangeModalTitle">Filter invoices by Custom Range</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<vc:icon symbol="close" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label for="dtpStartDate" class="col-sm-3 col-form-label">Start Date</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input id="dtpStartDate" class="form-control flatdtpicker" type="datetime-local"
|
||||
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
|
||||
placeholder="Start Date" />
|
||||
<button type="button" class="btn btn-primary input-group-clear" title="Clear">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">End Date</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input id="dtpEndDate" class="form-control flatdtpicker" type="datetime-local"
|
||||
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
|
||||
placeholder="End Date" />
|
||||
<button type="button" class="btn btn-primary input-group-clear" title="Clear">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btnCustomRangeDate" type="button" class="btn btn-primary">Filter</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btnCustomRangeDate" type="button" class="btn btn-primary">Filter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row collapse" id="help">
|
||||
<div class="col @(Model.Total > 0 ? "pt-3 pb-lg-5" : "")">
|
||||
<div id="help" class="row collapse">
|
||||
<div class="col-xl-8 pb-3">
|
||||
<p>
|
||||
You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.
|
||||
Be sure to split your search parameters with comma, for example:<br />
|
||||
@ -304,17 +264,57 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<form class="col-lg-6 col-xl-8 mb-4" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" method="get">
|
||||
<input type="hidden" asp-for="Count" />
|
||||
<input asp-for="TimezoneOffset" type="hidden" />
|
||||
<div class="input-group">
|
||||
<a href="#help" class="input-group-text text-secondary text-decoration-none" data-bs-toggle="collapse">
|
||||
<span class="fa fa-filter"></span>
|
||||
</a>
|
||||
<input asp-for="SearchTerm" class="form-control" />
|
||||
<button type="submit" class="btn btn-secondary" title="Search invoice">
|
||||
<span class="fa fa-search"></span> Search
|
||||
</button>
|
||||
<button type="button" id="SearchOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
|
||||
@if (Model.Total > 0)
|
||||
{
|
||||
<form method="post" id="MassAction" asp-action="MassAction" class="mt-lg-n5">
|
||||
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="SearchOptionsToggle">
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="status:invalid@{@storeIds}">Invalid Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="status:processing,status:settled@{@storeIds}">Paid Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidLate@{@storeIds}">Paid Late Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidPartial@{@storeIds}">Paid Partial Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidOver@{@storeIds}">Paid Over Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="unusual:true@{@storeIds}">Unusual Invoices</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="includearchived:true@{@storeIds}" id="SearchOptionsIncludeArchived">Archived Invoices</a>
|
||||
<div role="separator" class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-timezoneoffset="0" asp-route-searchTerm="startDate:-24h@{@storeIds}">Last 24 hours</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-timezoneoffset="0" asp-route-searchTerm="startDate:-3d@{@storeIds}">Last 3 days</a>
|
||||
<a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-timezoneoffset="0" asp-route-searchTerm="startDate:-7d@{@storeIds}">Last 7 days</a>
|
||||
<button type="button" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#customRangeModal">Custom Range</button>
|
||||
<div role="separator" class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="?searchTerm=">Unfiltered</a>
|
||||
</div>
|
||||
</div>
|
||||
<span asp-validation-for="SearchTerm" class="text-danger"></span>
|
||||
</form>
|
||||
|
||||
<form method="post" id="MassAction" asp-action="MassAction" class="">
|
||||
<div class="d-inline-flex align-items-center pb-2 float-lg-end mb-2">
|
||||
<input type="hidden" name="storeId" value="@Model.StoreId" />
|
||||
<a href="https://docs.btcpayserver.org/Accounting/" class="ms-2 ms-lg-0 me-lg-2 order-1 order-lg-0" target="_blank" rel="noreferrer noopener">
|
||||
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
|
||||
</a>
|
||||
<span class="me-2">
|
||||
<button class="btn btn-secondary dropdown-toggle mb-1" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Actions
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="ActionsDropdownToggle">
|
||||
<button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="archive"><i class="fa fa-archive"></i> Archive</button>
|
||||
<button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="archive" id="ActionsDropdownArchive"><i class="fa fa-archive"></i> Archive</button>
|
||||
@if (Model.IncludeArchived)
|
||||
{
|
||||
<button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="unarchive" id="ActionsDropdownUnarchive"><i class="fa fa-archive"></i> Unarchive</button>
|
||||
}
|
||||
</div>
|
||||
</span>
|
||||
<span>
|
||||
@ -326,139 +326,140 @@
|
||||
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="json" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">JSON</a>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a href="https://docs.btcpayserver.org/Accounting/" class="ms-1" target="_blank" rel="noreferrer noopener">
|
||||
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
|
||||
</a>
|
||||
|
||||
<table id="invoices" class="table table-hover table-responsive-md mt-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:2rem;" class="only-for-js">
|
||||
<input id="selectAllCheckbox" type="checkbox" class="form-check-input"/>
|
||||
</th>
|
||||
<th style="min-width:90px;" class="col-md-auto">
|
||||
Date
|
||||
<a id="switchTimeFormat" href="#">
|
||||
<span class="fa fa-clock-o" title="Switch date format"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th style="max-width: 180px;">OrderId</th>
|
||||
<th>InvoiceId</th>
|
||||
<th style="min-width: 150px;">Status</th>
|
||||
<th style="text-align:right">Amount</th>
|
||||
<th style="text-align:right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var invoice in Model.Invoices)
|
||||
{
|
||||
<tr id="invoice_@invoice.InvoiceId" class="invoice-row">
|
||||
<td class="only-for-js">
|
||||
<input name="selectedItems" type="checkbox" class="selector form-check-input" value="@invoice.InvoiceId"/>
|
||||
</td>
|
||||
<td>
|
||||
<span class="switchTimeFormat" data-switch="@invoice.Date.ToTimeAgo()">
|
||||
@invoice.Date.ToBrowserDate()
|
||||
</span>
|
||||
</td>
|
||||
<td style="max-width: 180px;">
|
||||
@if (invoice.RedirectUrl != string.Empty)
|
||||
{
|
||||
<a href="@invoice.RedirectUrl" class="wraptext200" rel="noreferrer noopener">@invoice.OrderId</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@invoice.OrderId</span>
|
||||
}
|
||||
</td>
|
||||
<td>@invoice.InvoiceId</td>
|
||||
<td>
|
||||
@if (invoice.Details.Archived)
|
||||
{
|
||||
<span class="badge bg-warning">archived</span>
|
||||
}
|
||||
@if (invoice.CanMarkStatus)
|
||||
{
|
||||
<div id="pavpill_@invoice.InvoiceId" class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
|
||||
<span class="dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString() *@
|
||||
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
|
||||
{
|
||||
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString())
|
||||
;
|
||||
}
|
||||
@if (Model.Total > 0)
|
||||
{
|
||||
<div style="clear:both"></div>
|
||||
<div class="table-responsive">
|
||||
<table id="invoices" class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:2rem;" class="only-for-js">
|
||||
<input id="selectAllCheckbox" type="checkbox" class="form-check-input" />
|
||||
<th style="min-width:90px;" class="col-md-auto">
|
||||
Date
|
||||
<a id="switchTimeFormat" href="#">
|
||||
<span class="fa fa-clock-o" title="Switch date format"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th class="text-nowrap">Order Id</th>
|
||||
<th class="text-nowrap">Invoice Id</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var invoice in Model.Invoices)
|
||||
{
|
||||
<tr id="invoice_@invoice.InvoiceId" class="invoice-row">
|
||||
<td class="only-for-js">
|
||||
<input name="selectedItems" type="checkbox" class="selector form-check-input" value="@invoice.InvoiceId" />
|
||||
</td>
|
||||
<td>
|
||||
<span class="switchTimeFormat" data-switch="@invoice.Date.ToTimeAgo()">
|
||||
@invoice.Date.ToBrowserDate()
|
||||
</span>
|
||||
<div class="dropdown-menu pull-right">
|
||||
@if (invoice.CanMarkInvalid)
|
||||
{
|
||||
<button class="dropdown-item small cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="invalid">
|
||||
Mark as invalid <span class="fa fa-times"></span>
|
||||
</button>
|
||||
}
|
||||
@if (invoice.CanMarkSettled)
|
||||
{
|
||||
<button class="dropdown-item small cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="settled">
|
||||
Mark as settled <span class="fa fa-check-circle"></span>
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
<td style="max-width:120px;">
|
||||
@if (invoice.RedirectUrl != string.Empty)
|
||||
{
|
||||
<a href="@invoice.RedirectUrl" class="wraptextAuto" rel="noreferrer noopener">@invoice.OrderId</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@invoice.OrderId</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-break">@invoice.InvoiceId</td>
|
||||
<td>
|
||||
@if (invoice.Details.Archived)
|
||||
{
|
||||
<span class="badge bg-warning">archived</span>
|
||||
}
|
||||
@if (invoice.CanMarkStatus)
|
||||
{
|
||||
<div id="pavpill_@invoice.InvoiceId" class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
|
||||
<span class="dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString() *@
|
||||
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
|
||||
{
|
||||
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString())
|
||||
;
|
||||
}
|
||||
</span>
|
||||
<div class="dropdown-menu pull-right">
|
||||
@if (invoice.CanMarkInvalid)
|
||||
{
|
||||
<button class="dropdown-item small cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="invalid">
|
||||
Mark as invalid <span class="fa fa-times"></span>
|
||||
</button>
|
||||
}
|
||||
@if (invoice.CanMarkSettled)
|
||||
{
|
||||
<button class="dropdown-item small cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="settled">
|
||||
Mark as settled <span class="fa fa-check-circle"></span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
|
||||
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString().ToLower() *@
|
||||
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
|
||||
{
|
||||
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString())
|
||||
;
|
||||
}
|
||||
</span>
|
||||
}
|
||||
@foreach (var paymentType in invoice.Details.Payments.Select(payment => payment.GetPaymentMethodId()?.PaymentType).Distinct().Where(type => type != null && !string.IsNullOrEmpty(type.GetBadge())))
|
||||
{
|
||||
<span class="badge">@paymentType.GetBadge()</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-end text-nowrap">@invoice.AmountCurrency</td>
|
||||
<td class="text-end text-nowrap">
|
||||
@if (invoice.ShowCheckout)
|
||||
{
|
||||
<span>
|
||||
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId" class="invoice-checkout-link" id="invoice-checkout-@invoice.InvoiceId">Checkout</a>
|
||||
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId" class="showInvoice only-for-js" data-invoice-id="@invoice.InvoiceId">[^]</a>
|
||||
@if (!invoice.CanMarkStatus)
|
||||
{
|
||||
<span>-</span>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
|
||||
<a asp-action="Invoice" class="invoice-details-link" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
|
||||
<a class="only-for-js invoice-details-toggle" href="#">
|
||||
<span title="Invoice Details Toggle" class="fa fa-1x fa-angle-double-down"></span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="invoice_details_@invoice.InvoiceId" class="invoice-details-row" style="display:none;">
|
||||
<td colspan="99" class="border-top-0">
|
||||
<div style="margin-left: 15px; margin-bottom: 0;">
|
||||
@* Leaving this as partial because it abstracts complexity of Invoice Payments *@
|
||||
<partial name="ListInvoicesPaymentsPartial" model="(invoice.Details, true)" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
|
||||
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString().ToLower() *@
|
||||
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
|
||||
{
|
||||
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString())
|
||||
;
|
||||
}
|
||||
</span>
|
||||
}
|
||||
@foreach (var paymentType in invoice.Details.Payments.Select(payment => payment.GetPaymentMethodId()?.PaymentType).Distinct().Where(type => type != null && !string.IsNullOrEmpty(type.GetBadge())))
|
||||
{
|
||||
<span class="badge">@paymentType.GetBadge()</span>
|
||||
}
|
||||
</td>
|
||||
<td style="text-align:right">@invoice.AmountCurrency</td>
|
||||
<td style="text-align:right">
|
||||
@if (invoice.ShowCheckout)
|
||||
{
|
||||
<span>
|
||||
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId" class="invoice-checkout-link" id="invoice-checkout-@invoice.InvoiceId">Checkout</a>
|
||||
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId" class="showInvoice only-for-js" data-invoice-id="@invoice.InvoiceId">[^]</a>
|
||||
@if (!invoice.CanMarkStatus)
|
||||
{
|
||||
<span>-</span>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
|
||||
<a asp-action="Invoice" class="invoice-details-link" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
|
||||
<a class="only-for-js invoice-details-toggle" href="#">
|
||||
<span title="Invoice Details Toggle" class="fa fa-1x fa-angle-double-down"></span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="invoice_details_@invoice.InvoiceId" class="invoice-details-row" style="display:none;">
|
||||
<td colspan="99" class="border-top-0">
|
||||
<div style="margin-left: 15px; margin-bottom: 0;">
|
||||
@* Leaving this as partial because it abstracts complexity of Invoice Payments *@
|
||||
<partial name="ListInvoicesPaymentsPartial" model="(invoice.Details, true)"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<vc:pager view-model="Model"/>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary mt-3">
|
||||
There are no invoices matching your criteria.
|
||||
</p>
|
||||
}
|
||||
<vc:pager view-model="Model" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary mt-3">
|
||||
There are no invoices matching your criteria.
|
||||
</p>
|
||||
}
|
||||
</form>
|
||||
|
Loading…
Reference in New Issue
Block a user