btcpayserver/BTCPayServer/Views/Invoice/ListInvoices.cshtml

452 lines
24 KiB
Plaintext
Raw Normal View History

@using BTCPayServer.Payments
2020-07-17 06:24:41 +02:00
@model InvoicesModel
2017-09-13 08:47:34 +02:00
@{
ViewData["Title"] = "Invoices";
var storeIds = string.Join("", Model.StoreIds.Select(storeId => $",storeid:{storeId}"));
2018-11-09 09:13:45 +01:00
}
@section HeadScripts {
<script src="~/modal/btcpay.js" asp-append-version="true"></script>
2017-09-13 08:47:34 +02:00
}
@Html.HiddenFor(a => a.Count)
2017-09-13 08:47:34 +02:00
<section>
<div class="container">
@if (TempData.HasStatusMessage())
{
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" />
</div>
</div>
}
2017-09-13 08:47:34 +02:00
<div class="row">
2019-11-01 21:55:05 +01:00
<div class="col-lg-12 section-heading">
<h2>@ViewData["Title"]</h2>
<hr class="primary">
<p>Create, search or pay an invoice.</p>
</div>
</div>
<div class="row">
<div class="col-12 col-sm-4 col-lg-6 mb-3">
2020-10-17 22:03:28 +02:00
<a id="CreateNewInvoice" asp-action="CreateInvoice" class="btn btn-primary mb-1">
<span class="fa fa-plus"></span>
Create an invoice
</a>
</div>
<div class="col-12 col-sm-8 col-lg-6 mb-3">
<form asp-action="ListInvoices" method="get">
<input type="hidden" asp-for="Count"/>
<input asp-for="TimezoneOffset" type="hidden"/>
<div class="input-group">
<div class="input-group-prepend">
<a href="#help" class="input-group-text text-secondary text-decoration-none" data-toggle="collapse">
<span class="fa fa-filter"></span>
</a>
</div>
<input asp-for="SearchTerm" class="form-control"/>
<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="/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>
<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="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>
</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>
2019-05-12 01:31:52 +02:00
</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>
2019-05-12 01:31:52 +02:00
</div>
</div>
</div>
<div class="modal-footer">
<button id="btnCustomRangeDate" type="button" class="btn btn-primary">Filter</button>
</div>
</div>
2019-05-12 01:31:52 +02:00
</div>
</div>
<script type="text/javascript">
$('#btnCustomRangeDate').on('click', function (sender) {
var filterString = "";
2019-05-12 01:31:52 +02:00
var dtpStartDate = $("#dtpStartDate").val();
if (dtpStartDate !== null && dtpStartDate !== "") {
filterString = "startDate%3A" + dtpStartDate;
}
var dtpEndDate = $("#dtpEndDate").val();
if (dtpEndDate !== null && dtpEndDate !== "") {
if (filterString !== "") {
filterString += ",";
}
filterString += "endDate%3A" + dtpEndDate;
}
2019-05-12 01:31:52 +02:00
if (filterString !== "") {
var redirectUri = "/invoices?Count=" + $("#Count").val() +
"&timezoneoffset=" + $("#TimezoneOffset").val() +
"&SearchTerm=" + filterString;
window.location.href = redirectUri;
} else {
$("#dtpStartDate").next().trigger("focus");
}
})
</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>
2019-05-12 01:31:52 +02:00
@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>
2019-05-12 01:31:52 +02:00
<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>
2019-05-12 01:31:52 +02:00
<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>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th style="width:2rem;" class="only-for-js">
<input id="selectAllCheckbox" type="checkbox" onclick="selectAll(this);"/>
</th>
<th style="min-width:90px;" class="col-md-auto">
Date
<a href="javascript:switchTimeFormat()">
<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>
<td class="only-for-js">
<input name="selectedItems" type="checkbox" class="selector" 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">@invoice.OrderId</a>
}
else
{
<span>@invoice.OrderId</span>
}
</td>
<td>@invoice.InvoiceId</td>
<td>
@if (invoice.Details.Archived)
{
<span class="badge badge-warning">archived</span>
}
@if (invoice.CanMarkStatus)
{
<div id="pavpill_@invoice.InvoiceId" class="badge badge-@invoice.Status.ToString().ToLower()">
<span class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@invoice.Status.ToString()
</span>
<div class="dropdown-menu pull-right">
@if (invoice.CanMarkInvalid)
{
<button class="dropdown-item small cursorPointer" onclick="changeInvoiceState(this, '@invoice.InvoiceId', 'invalid')">
Mark as invalid <span class="fa fa-times"></span>
</button>
}
@if (invoice.CanMarkComplete)
{
<button class="dropdown-item small cursorPointer" onclick="changeInvoiceState(this, '@invoice.InvoiceId', 'complete')">
Mark as complete <span class="fa fa-check-circle"></span>
</button>
}
</div>
</div>
}
else
{
<span class="badge badge-@invoice.Status.ToString().ToLower()">
@invoice.Status.ToString().ToLower()
</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" class="invoice-checkout-link" id="invoice-checkout-@invoice.InvoiceId" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a>
<a href="javascript:btcpay.showInvoice('@invoice.InvoiceId')">[^]</a>
@if (!invoice.CanMarkStatus)
{
<span>-</span>
}
</span>
}
&nbsp;
<a asp-action="Invoice" class="invoice-details-link" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
<a href="javascript:void(0);" onclick="detailsToggle(this, '@invoice.InvoiceId')">
<span title="Invoice Details Toggle" class="fa fa-1x fa-angle-double-down"></span>
</a>
</td>
</tr>
<tr id="invoice_@invoice.InvoiceId" style="display:none;">
<td colspan="99" class="border-top-0">
<div style="margin-left: 15px; margin-bottom: 0;">
@* Leaving this as partial because it abstracts complexity of Invoice Payments *@
<partial name="ListInvoicesPaymentsPartial" model="(invoice.Details, true)"/>
</div>
</td>
</tr>
}
</tbody>
</table>
<vc:pager view-model="Model"></vc:pager>
</form>
}
else
{
<p class="text-secondary mt-3">
There are no invoices matching your criteria.
</p>
}
</div>
<script type="text/javascript">
$(function () {
2020-07-20 00:04:29 +02:00
var timezoneOffset = new Date().getTimezoneOffset();
2019-05-09 19:48:32 +02:00
$(".export-link, a.dropdown-item").each(function () {
this.href = this.href.replace("timezoneoffset=0", "timezoneoffset=" + timezoneOffset);
});
2019-05-09 19:48:32 +02:00
});
function getDateStringWithOffset(hoursDiff) {
var datenow = new Date();
var newDate = new Date(datenow.getTime() - (hoursDiff * 60 * 60 * 1000));
var str = newDate.toLocaleDateString() + " " + newDate.toLocaleTimeString();
return str;
}
function detailsToggle(sender, invoiceId) {
$("#invoice_" + invoiceId).toggle(0, function () {
var detailsRow = $(this);
var btnToggle = $(sender).children().first();
if (detailsRow.is(':visible')) {
btnToggle.removeClass('fa-angle-double-down').addClass('fa-angle-double-up');
} else {
btnToggle.removeClass('fa-angle-double-up').addClass('fa-angle-double-down');
}
});
return false;
}
function changeInvoiceState(sender, invoiceId, newState) {
var pavpill = $("#pavpill_" + invoiceId);
var originalHtml = pavpill.html();
pavpill.html("<span class='fa fa-bitcoin fa-spin' style='margin-left:16px;'></span>");
$.post("invoices/" + invoiceId + "/changestate/" + newState)
.done(function (data) {
var statusHtml = "<span class='badge badge-" + newState + "'>" + data.statusString + " <span class='fa fa-check'></span></span>";
pavpill.replaceWith(statusHtml);
})
.fail(function (data) {
pavpill.html(originalHtml.replace("dropdown-menu pull-right show", "dropdown-menu pull-right"));
alert("Invoice state update failed");
});
}
</script>
<style type="text/css">
.invoice-payments {
padding-left: 2rem;
}
.invoice-payments h3 {
font-size: 15px;
font-weight: bold;
}
.wraptext200 {
max-width: 200px;
text-overflow: ellipsis;
overflow: hidden;
display: block;
white-space: nowrap;
}
2019-05-01 22:33:46 +02:00
.pavpill {
display: inline-block;
padding: 0.3em 0.5em;
font-size: 85%;
font-weight: 500;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25rem;
}
.badge .dropdown-toggle {
cursor: pointer;
padding: 0;
}
2019-05-01 22:33:46 +02:00
.dropdown-item {
cursor: pointer;
}
.badge-new {
2019-05-01 22:33:46 +02:00
background: #d4edda;
color: #000;
}
.badge-expired {
2019-05-01 22:33:46 +02:00
background: #eee;
color: #000;
}
.badge-invalid {
2019-05-01 22:33:46 +02:00
background: #c94a47;
color: #fff;
}
.badge-confirmed,
.badge-paid {
2019-05-01 22:33:46 +02:00
background: #f1c332;
color: #000;
}
.badge-complete {
2019-05-01 22:33:46 +02:00
background: #329f80;
color: #fff;
}
</style>
</section>