Add ability to set invoice status from details page (#2923)

* Add ability to set invoice status from details page

* Remove unnecessary "using" statements

* Add print styles

* Fix Safari issues

* Simplify JS

* Update status badge class names

* Update dropdown toggle padding

* Adjust dropdown menu padding
This commit is contained in:
Umar Bolatov
2021-10-05 22:49:57 -07:00
committed by GitHub
parent 150e4b842c
commit 31c2a80758
4 changed files with 79 additions and 9 deletions

View File

@@ -13,16 +13,12 @@ using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Filters; using BTCPayServer.Filters;
using BTCPayServer.Logging;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating; using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Invoices.Export; using BTCPayServer.Services.Invoices.Export;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
@@ -104,6 +100,7 @@ namespace BTCPayServer.Controllers
return NotFound(); return NotFound();
var store = await _StoreRepository.FindStore(invoice.StoreId); var store = await _StoreRepository.FindStore(invoice.StoreId);
var invoiceState = invoice.GetInvoiceState();
var model = new InvoiceDetailsModel() var model = new InvoiceDetailsModel()
{ {
StoreId = store.Id, StoreId = store.Id,
@@ -111,7 +108,7 @@ namespace BTCPayServer.Controllers
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }), StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
PaymentRequestLink = Url.Action(nameof(PaymentRequestController.ViewPaymentRequest), "PaymentRequest", new { id = invoice.Metadata.PaymentRequestId }), PaymentRequestLink = Url.Action(nameof(PaymentRequestController.ViewPaymentRequest), "PaymentRequest", new { id = invoice.Metadata.PaymentRequestId }),
Id = invoice.Id, Id = invoice.Id,
State = invoice.GetInvoiceState().ToString(), State = invoiceState.ToString(),
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" : invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
@@ -129,11 +126,13 @@ namespace BTCPayServer.Controllers
Events = invoice.Events, Events = invoice.Events,
PosData = PosDataParser.ParsePosData(invoice.Metadata.PosData), PosData = PosDataParser.ParsePosData(invoice.Metadata.PosData),
Archived = invoice.Archived, Archived = invoice.Archived,
CanRefund = CanRefund(invoice.GetInvoiceState()), CanRefund = CanRefund(invoiceState),
ShowCheckout = invoice.Status == InvoiceStatusLegacy.New, ShowCheckout = invoice.Status == InvoiceStatusLegacy.New,
Deliveries = (await _InvoiceRepository.GetWebhookDeliveries(invoiceId)) Deliveries = (await _InvoiceRepository.GetWebhookDeliveries(invoiceId))
.Select(c => new Models.StoreViewModels.DeliveryViewModel(c)) .Select(c => new Models.StoreViewModels.DeliveryViewModel(c))
.ToList() .ToList(),
CanMarkInvalid = invoiceState.CanMarkInvalid(),
CanMarkComplete = invoiceState.CanMarkComplete(),
}; };
model.Addresses = invoice.HistoricalAddresses.Select(h => model.Addresses = invoice.HistoricalAddresses.Select(h =>
new InvoiceDetailsModel.AddressModel new InvoiceDetailsModel.AddressModel

View File

@@ -126,5 +126,8 @@ namespace BTCPayServer.Models.InvoicingModels
public bool Archived { get; set; } public bool Archived { get; set; }
public bool CanRefund { get; set; } public bool CanRefund { get; set; }
public bool ShowCheckout { get; set; } public bool ShowCheckout { get; set; }
public bool CanMarkComplete { get; set; }
public bool CanMarkInvalid { get; set; }
public bool CanMarkStatus => CanMarkComplete || CanMarkInvalid;
} }
} }

View File

@@ -11,6 +11,38 @@
</style> </style>
} }
@section PageFootContent {
<script>
function changeInvoiceState(invoiceId, newState) {
var toggleButton = $("#markStatusDropdownMenuButton");
toggleButton.attr("disabled", "disabled");
$.post(invoiceId + "/changestate/" + newState)
.done(function (data) {
var alertClassModifier = {
"complete (marked)": 'success',
"invalid (marked)": 'danger'
}[data.statusString];
var statusHtml = "<span class='fs-6 fw-normal badge bg-" + alertClassModifier + "'>" + data.statusString + " <span class='fa fa-check'></span></span>"
toggleButton.replaceWith(statusHtml);
})
.fail(function () {
toggleButton.removeAttr("disabled");
alert("Invoice state update failed");
});
}
document.addEventListener("DOMContentLoaded", function () {
$("[data-change-invoice-status-button]").click(function (e) {
var id = e.currentTarget.getAttribute('data-id');
var status = e.currentTarget.getAttribute('data-status');
changeInvoiceState(id, status);
});
});
</script>
}
<section class="invoice-details"> <section class="invoice-details">
<div class="container"> <div class="container">
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
@@ -19,7 +51,6 @@
<h2 class="col-xs-12 col-lg-6 mb-4 mb-lg-0">@ViewData["Title"]</h2> <h2 class="col-xs-12 col-lg-6 mb-4 mb-lg-0">@ViewData["Title"]</h2>
<div class="col-xs-12 col-lg-6 mb-2 mb-lg-0 text-lg-end"> <div class="col-xs-12 col-lg-6 mb-2 mb-lg-0 text-lg-end">
<div class="d-inline-flex"> <div class="d-inline-flex">
@if (Model.ShowCheckout) @if (Model.ShowCheckout)
{ {
<a asp-action="Checkout" class="invoice-checkout-link btn btn-primary text-nowrap ms-2" asp-route-invoiceId="@Model.Id"> <a asp-action="Checkout" class="invoice-checkout-link btn btn-primary text-nowrap ms-2" asp-route-invoiceId="@Model.Id">
@@ -92,7 +123,33 @@
</tr> </tr>
<tr> <tr>
<th>State</th> <th>State</th>
<td>@Model.State</td> <td>
@if (Model.CanMarkStatus)
{
<div class="dropdown">
<button class="btn btn-secondary btn-sm dropdown-toggle py-1 px-2" type="button" id="markStatusDropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@Model.State
</button>
<div class="dropdown-menu" aria-labelledby="markStatusDropdownMenuButton">
@if (Model.CanMarkInvalid)
{
<a class="dropdown-item" href="#" data-id="@Model.Id" data-status="invalid" data-change-invoice-status-button>
Mark as invalid <span class="fa fa-times"></span>
</a>
}
@if (Model.CanMarkComplete)
{
<a class="dropdown-item" href="#" data-id="@Model.Id" data-status="complete" data-change-invoice-status-button>
Mark as complete <span class="fa fa-check-circle"></span>
</a>
}
</div>
</div>
}
else {
@Model.State
}
</td>
</tr> </tr>
<tr> <tr>
<th>Created date</th> <th>Created date</th>

View File

@@ -337,6 +337,17 @@ h2 small .fa-question-circle-o {
.toasted-container { .toasted-container {
display: none !important; display: none !important;
} }
#markStatusDropdownMenuButton {
border: 0;
background: transparent;
padding: 0 !important;
color: inherit;
font-weight: var(--btcpay-font-weight-normal);
font-size: var(--btcpay-body-font-size);
}
#markStatusDropdownMenuButton::after {
content: none;
}
} }
/* Richtext editor */ /* Richtext editor */