mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-12 10:30:47 +01:00
Improve data display on receipt (#5896)
Once more an improvement for the receipt, which also fixes #5882: - Unify data displayed on the web and print version - Split cart and additional data and ensure additional data is displayed - Do not display extra subtotal row if there are no tips or discounts - Make PosData partial more universal and backwards-compatible by using case insensitive key lookups
This commit is contained in:
parent
aeb836da76
commit
56d57bbd84
9 changed files with 181 additions and 87 deletions
|
@ -2620,9 +2620,9 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
// Receipt
|
// Receipt
|
||||||
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
||||||
var additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table"));
|
var cartData = s.Driver.FindElement(By.CssSelector("#CartData table"));
|
||||||
var items = additionalData.FindElements(By.CssSelector("tbody tr"));
|
var items = cartData.FindElements(By.CssSelector("tbody tr"));
|
||||||
var sums = additionalData.FindElements(By.CssSelector("tfoot tr"));
|
var sums = cartData.FindElements(By.CssSelector("tfoot tr"));
|
||||||
Assert.Equal(2, items.Count);
|
Assert.Equal(2, items.Count);
|
||||||
Assert.Equal(4, sums.Count);
|
Assert.Equal(4, sums.Count);
|
||||||
Assert.Contains("Manual entry 1", items[0].FindElement(By.CssSelector("th")).Text);
|
Assert.Contains("Manual entry 1", items[0].FindElement(By.CssSelector("th")).Text);
|
||||||
|
@ -2676,21 +2676,19 @@ namespace BTCPayServer.Tests
|
||||||
// Receipt
|
// Receipt
|
||||||
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
||||||
|
|
||||||
additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table"));
|
cartData = s.Driver.FindElement(By.CssSelector("#CartData table"));
|
||||||
items = additionalData.FindElements(By.CssSelector("tbody tr"));
|
items = cartData.FindElements(By.CssSelector("tbody tr"));
|
||||||
sums = additionalData.FindElements(By.CssSelector("tfoot tr"));
|
sums = cartData.FindElements(By.CssSelector("tfoot tr"));
|
||||||
Assert.Equal(3, items.Count);
|
Assert.Equal(3, items.Count);
|
||||||
Assert.Equal(2, sums.Count);
|
Assert.Equal(1, sums.Count);
|
||||||
Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector("th")).Text);
|
Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector("th")).Text);
|
||||||
Assert.Contains("1 x 1,00 € = 1,00 €", items[0].FindElement(By.CssSelector("td")).Text);
|
Assert.Contains("1 x 1,00 € = 1,00 €", items[0].FindElement(By.CssSelector("td")).Text);
|
||||||
Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector("th")).Text);
|
Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector("th")).Text);
|
||||||
Assert.Contains("2 x 1,00 € = 2,00 €", items[1].FindElement(By.CssSelector("td")).Text);
|
Assert.Contains("2 x 1,00 € = 2,00 €", items[1].FindElement(By.CssSelector("td")).Text);
|
||||||
Assert.Contains("Manual entry 1", items[2].FindElement(By.CssSelector("th")).Text);
|
Assert.Contains("Manual entry 1", items[2].FindElement(By.CssSelector("th")).Text);
|
||||||
Assert.Contains("1,23 €", items[2].FindElement(By.CssSelector("td")).Text);
|
Assert.Contains("1,23 €", items[2].FindElement(By.CssSelector("td")).Text);
|
||||||
Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector("th")).Text);
|
Assert.Contains("Total", sums[0].FindElement(By.CssSelector("th")).Text);
|
||||||
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector("td")).Text);
|
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector("td")).Text);
|
||||||
Assert.Contains("Total", sums[1].FindElement(By.CssSelector("th")).Text);
|
|
||||||
Assert.Contains("4,23 €", sums[1].FindElement(By.CssSelector("td")).Text);
|
|
||||||
|
|
||||||
// Guest user can access recent transactions
|
// Guest user can access recent transactions
|
||||||
s.GoToHome();
|
s.GoToHome();
|
||||||
|
@ -2837,9 +2835,9 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
// Receipt
|
// Receipt
|
||||||
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
||||||
var additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table"));
|
var cartData = s.Driver.FindElement(By.CssSelector("#CartData table"));
|
||||||
var items = additionalData.FindElements(By.CssSelector("tbody tr"));
|
var items = cartData.FindElements(By.CssSelector("tbody tr"));
|
||||||
var sums = additionalData.FindElements(By.CssSelector("tfoot tr"));
|
var sums = cartData.FindElements(By.CssSelector("tfoot tr"));
|
||||||
Assert.Equal(7, items.Count);
|
Assert.Equal(7, items.Count);
|
||||||
Assert.Equal(4, sums.Count);
|
Assert.Equal(4, sums.Count);
|
||||||
Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector("th")).Text);
|
Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector("th")).Text);
|
||||||
|
|
|
@ -164,9 +164,9 @@ namespace BTCPayServer.Controllers
|
||||||
model.StillDue = details.StillDue;
|
model.StillDue = details.StillDue;
|
||||||
model.HasRates = details.HasRates;
|
model.HasRates = details.HasRates;
|
||||||
|
|
||||||
if (additionalData.ContainsKey("receiptData"))
|
if (additionalData.TryGetValue("receiptData", out object? receiptData))
|
||||||
{
|
{
|
||||||
model.ReceiptData = (Dictionary<string, object>)additionalData["receiptData"];
|
model.ReceiptData = (Dictionary<string, object>)receiptData;
|
||||||
additionalData.Remove("receiptData");
|
additionalData.Remove("receiptData");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,14 +228,38 @@ namespace BTCPayServer.Controllers
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
JToken? receiptData = null;
|
var metaData = PosDataParser.ParsePosData(i.Metadata?.ToJObject());
|
||||||
i.Metadata?.AdditionalData?.TryGetValue("receiptData", out receiptData);
|
var additionalData = metaData
|
||||||
|
.Where(dict => !InvoiceAdditionalDataExclude.Contains(dict.Key))
|
||||||
|
.ToDictionary(dict => dict.Key, dict => dict.Value);
|
||||||
|
|
||||||
|
// Split receipt data into cart and additional data
|
||||||
|
if (additionalData.TryGetValue("receiptData", out object? combinedReceiptData))
|
||||||
|
{
|
||||||
|
var receiptData = new Dictionary<string, object>((Dictionary<string, object>)combinedReceiptData, StringComparer.OrdinalIgnoreCase);
|
||||||
|
string[] cartKeys = ["cart", "subtotal", "discount", "tip", "total"];
|
||||||
|
// extract cart data and lowercase keys to handle data uniformly in PosData partial
|
||||||
|
if (receiptData.Keys.Any(key => cartKeys.Contains(key.ToLowerInvariant())))
|
||||||
|
{
|
||||||
|
vm.CartData = new Dictionary<string, object>();
|
||||||
|
foreach (var key in cartKeys)
|
||||||
|
{
|
||||||
|
if (!receiptData.ContainsKey(key)) continue;
|
||||||
|
// add it to cart data and remove it from the general data
|
||||||
|
vm.CartData.Add(key.ToLowerInvariant(), receiptData[key]);
|
||||||
|
receiptData.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// assign the rest to additional data
|
||||||
|
if (receiptData.Any())
|
||||||
|
{
|
||||||
|
vm.AdditionalData = receiptData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var payments = ViewPaymentRequestViewModel.PaymentRequestInvoicePayment.GetViewModels(i, _displayFormatter, _transactionLinkProviders, _handlers);
|
var payments = ViewPaymentRequestViewModel.PaymentRequestInvoicePayment.GetViewModels(i, _displayFormatter, _transactionLinkProviders, _handlers);
|
||||||
|
|
||||||
vm.Amount = i.PaidAmount.Net;
|
vm.Amount = i.PaidAmount.Net;
|
||||||
vm.Payments = receipt.ShowPayments is false ? null : payments;
|
vm.Payments = receipt.ShowPayments is false ? null : payments;
|
||||||
vm.AdditionalData = PosDataParser.ParsePosData(receiptData);
|
|
||||||
|
|
||||||
return View(print ? "InvoiceReceiptPrint" : "InvoiceReceipt", vm);
|
return View(print ? "InvoiceReceiptPrint" : "InvoiceReceipt", vm);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
public DateTimeOffset Timestamp { get; set; }
|
public DateTimeOffset Timestamp { get; set; }
|
||||||
public Dictionary<string, object> AdditionalData { get; set; }
|
public Dictionary<string, object> AdditionalData { get; set; }
|
||||||
|
public Dictionary<string, object> CartData { get; set; }
|
||||||
public ReceiptOptions ReceiptOptions { get; set; }
|
public ReceiptOptions ReceiptOptions { get; set; }
|
||||||
public List<ViewPaymentRequestViewModel.PaymentRequestInvoicePayment> Payments { get; set; }
|
public List<ViewPaymentRequestViewModel.PaymentRequestInvoicePayment> Payments { get; set; }
|
||||||
public string OrderUrl { get; set; }
|
public string OrderUrl { get; set; }
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
@model (Dictionary<string, object> Items, int Level)
|
@model (Dictionary<string, object> Items, int Level)
|
||||||
|
|
||||||
@functions {
|
@functions {
|
||||||
|
@ -10,14 +11,20 @@
|
||||||
|
|
||||||
@if (Model.Items.Any())
|
@if (Model.Items.Any())
|
||||||
{
|
{
|
||||||
var hasCart = Model.Items.ContainsKey("Cart");
|
@* Use titlecase and lowercase versions for backwards-compatibility *@
|
||||||
|
string[] cartKeys = ["cart", "subtotal", "discount", "tip", "total"];
|
||||||
<table class="table my-0" v-pre>
|
<table class="table my-0" v-pre>
|
||||||
@if (hasCart || (Model.Items.ContainsKey("Subtotal") && Model.Items.ContainsKey("Total")))
|
@if (Model.Items.Keys.Any(key => cartKeys.Contains(key.ToLowerInvariant())))
|
||||||
{
|
{
|
||||||
@if (hasCart)
|
_ = Model.Items.TryGetValue("cart", out var cart) || Model.Items.TryGetValue("Cart", out cart);
|
||||||
|
var hasTotal = Model.Items.TryGetValue("total", out var total) || Model.Items.TryGetValue("Total", out total);
|
||||||
|
var hasSubtotal = Model.Items.TryGetValue("subtotal", out var subtotal) || Model.Items.TryGetValue("subTotal", out subtotal) || Model.Items.TryGetValue("Subtotal", out subtotal);
|
||||||
|
var hasDiscount = Model.Items.TryGetValue("discount", out var discount) || Model.Items.TryGetValue("Discount", out discount);
|
||||||
|
var hasTip = Model.Items.TryGetValue("tip", out var tip) || Model.Items.TryGetValue("Tip", out tip);
|
||||||
|
if (cart is Dictionary<string, object> { Keys.Count: > 0 } cartDict)
|
||||||
{
|
{
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var (key, value) in (Dictionary<string, object>)Model.Items["Cart"])
|
@foreach (var (key, value) in cartDict)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<th>@key</th>
|
<th>@key</th>
|
||||||
|
@ -26,35 +33,46 @@
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
}
|
}
|
||||||
<tfoot style="border-top-width:@(hasCart ? "3px" : "0")">
|
else if (cart is ICollection<object> { Count: > 0 } cartCollection)
|
||||||
@if (Model.Items.ContainsKey("Subtotal"))
|
{
|
||||||
|
<tbody>
|
||||||
|
@foreach (var value in cartCollection)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<th>Subtotal</th>
|
<td>@value</td>
|
||||||
<td class="text-end">@Model.Items["Subtotal"]</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
@if (Model.Items.ContainsKey("Discount"))
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<th>Discount</th>
|
|
||||||
<td class="text-end">@Model.Items["Discount"]</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
@if (Model.Items.ContainsKey("Tip"))
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<th>Tip</th>
|
|
||||||
<td class="text-end">@Model.Items["Tip"]</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
@if (Model.Items.ContainsKey("Total"))
|
|
||||||
{
|
|
||||||
<tr style="border-top-width:3px">
|
|
||||||
<th>Total</th>
|
|
||||||
<td class="text-end">@Model.Items["Total"]</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
</tbody>
|
||||||
|
}
|
||||||
|
<tfoot style="border-top-width:0">
|
||||||
|
@if (hasSubtotal && (hasDiscount || hasTip))
|
||||||
|
{
|
||||||
|
<tr style="border-top-width:3px">
|
||||||
|
<th>Subtotal</th>
|
||||||
|
<td class="text-end">@subtotal</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@if (hasDiscount)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<th>Discount</th>
|
||||||
|
<td class="text-end">@discount</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@if (hasTip)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<th>Tip</th>
|
||||||
|
<td class="text-end">@tip</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@if (hasTotal)
|
||||||
|
{
|
||||||
|
<tr style="border-top-width:3px">
|
||||||
|
<th>Total</th>
|
||||||
|
<td class="text-end">@total</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
</tfoot>
|
</tfoot>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
@ -422,7 +422,7 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (Model.ReceiptData != null && Model.ReceiptData.Any())
|
@if (Model.ReceiptData?.Any() is true)
|
||||||
{
|
{
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-3">
|
<h3 class="mb-3">
|
||||||
|
@ -434,7 +434,7 @@
|
||||||
<partial name="PosData" model="(Model.ReceiptData, 1)" />
|
<partial name="PosData" model="(Model.ReceiptData, 1)" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (Model.AdditionalData != null && Model.AdditionalData.Any())
|
@if (Model.AdditionalData?.Any() is true)
|
||||||
{
|
{
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-3">
|
<h3 class="mb-3">
|
||||||
|
|
|
@ -35,8 +35,8 @@
|
||||||
#InvoiceSummary { gap: var(--btcpay-space-l); }
|
#InvoiceSummary { gap: var(--btcpay-space-l); }
|
||||||
#PaymentDetails table tbody tr:first-child td { padding-top: 1rem; }
|
#PaymentDetails table tbody tr:first-child td { padding-top: 1rem; }
|
||||||
#PaymentDetails table tbody:not(:last-child) tr:last-child > th,td { padding-bottom: 1rem; }
|
#PaymentDetails table tbody:not(:last-child) tr:last-child > th,td { padding-bottom: 1rem; }
|
||||||
#posData td > table:last-child { margin-bottom: 0 !important; }
|
#AdditionalData td > table:last-child, #CartData td > table:last-child { margin-bottom: 0 !important; }
|
||||||
#posData table > tbody > tr:first-child > td > h4 { margin-top: 0 !important; }
|
#AdditionalData table > tbody > tr:first-child > td > h4, #CartData table > tbody > tr:first-child > td > h4 { margin-top: 0 !important; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-vh-100">
|
<body class="min-vh-100">
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
{
|
{
|
||||||
if (Model.ReceiptOptions.ShowQR is true)
|
if (Model.ReceiptOptions.ShowQR is true)
|
||||||
{
|
{
|
||||||
<vc:qr-code data="@Context.Request.GetCurrentUrl()"></vc:qr-code>
|
<vc:qr-code data="@Context.Request.GetCurrentUrl()" />
|
||||||
}
|
}
|
||||||
<div class="d-flex gap-4 mb-0 flex-fill">
|
<div class="d-flex gap-4 mb-0 flex-fill">
|
||||||
<dl class="d-flex flex-column gap-4 mb-0 flex-fill">
|
<dl class="d-flex flex-column gap-4 mb-0 flex-fill">
|
||||||
|
@ -102,6 +102,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
if (Model.CartData?.Any() is true)
|
||||||
|
{
|
||||||
|
<div id="CartData" class="tile">
|
||||||
|
<h2 class="h4 mb-3">Cart</h2>
|
||||||
|
<div class="table-responsive my-0">
|
||||||
|
<partial name="PosData" model="(Model.CartData, 1)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
if (Model.Payments?.Any() is true)
|
if (Model.Payments?.Any() is true)
|
||||||
{
|
{
|
||||||
<div id="PaymentDetails" class="tile">
|
<div id="PaymentDetails" class="tile">
|
||||||
|
|
|
@ -90,6 +90,7 @@
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
var hasCart = Model.CartData?.Any() is true;
|
||||||
<div id="PaymentDetails">
|
<div id="PaymentDetails">
|
||||||
<div class="my-2 text-center small">
|
<div class="my-2 text-center small">
|
||||||
@if (!string.IsNullOrEmpty(Model.OrderId))
|
@if (!string.IsNullOrEmpty(Model.OrderId))
|
||||||
|
@ -99,19 +100,29 @@
|
||||||
@Model.Timestamp.ToBrowserDate()
|
@Model.Timestamp.ToBrowserDate()
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-borderless table-sm small my-0">
|
<table class="table table-borderless table-sm small my-0">
|
||||||
<tr>
|
@if (Model.AdditionalData?.Any() is true)
|
||||||
<td class="text-nowrap text-secondary">Total</td>
|
|
||||||
<td class="text-end fw-semibold">@DisplayFormatter.Currency(Model.Amount, Model.Currency, DisplayFormatter.CurrencyFormat.Symbol)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
|
||||||
</tr>
|
|
||||||
@if (Model.AdditionalData?.Any() is true &&
|
|
||||||
(Model.AdditionalData.ContainsKey("Cart") || Model.AdditionalData.ContainsKey("Discount") || Model.AdditionalData.ContainsKey("Tip")))
|
|
||||||
{
|
{
|
||||||
@if (Model.AdditionalData.ContainsKey("Cart"))
|
@foreach (var (key, value) in Model.AdditionalData)
|
||||||
{
|
{
|
||||||
@foreach (var (key, value) in (Dictionary<string, object>)Model.AdditionalData["Cart"])
|
<tr>
|
||||||
|
<td class="text-secondary">@key</td>
|
||||||
|
<td class="text-end">@value</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@if (hasCart)
|
||||||
|
{
|
||||||
|
_ = Model.CartData.TryGetValue("cart", out var cart) || Model.CartData.TryGetValue("Cart", out cart);
|
||||||
|
var hasTotal = Model.CartData.TryGetValue("total", out var total) || Model.CartData.TryGetValue("Total", out total);
|
||||||
|
var hasSubtotal = Model.CartData.TryGetValue("subtotal", out var subtotal) || Model.CartData.TryGetValue("subTotal", out subtotal) || Model.CartData.TryGetValue("Subtotal", out subtotal);
|
||||||
|
var hasDiscount = Model.CartData.TryGetValue("discount", out var discount) || Model.CartData.TryGetValue("Discount", out discount);
|
||||||
|
var hasTip = Model.CartData.TryGetValue("tip", out var tip) || Model.CartData.TryGetValue("Tip", out tip);
|
||||||
|
if (cart is Dictionary<string, object> { Keys.Count: > 0 } cartDict)
|
||||||
|
{
|
||||||
|
@foreach (var (key, value) in cartDict)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-secondary">@key</td>
|
<td class="text-secondary">@key</td>
|
||||||
|
@ -119,33 +130,62 @@
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if (Model.AdditionalData.ContainsKey("Subtotal"))
|
else if (cart is ICollection<object> { Count: > 0 } cartCollection)
|
||||||
|
{
|
||||||
|
@foreach (var value in cartCollection)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="text-end">@value</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasSubtotal && (hasDiscount || hasTip))
|
||||||
{
|
{
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-secondary">Subtotal</td>
|
<td class="text-secondary">Subtotal</td>
|
||||||
<td class="text-end">@Model.AdditionalData["Subtotal"]</td>
|
<td class="text-end">@subtotal</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@if (Model.AdditionalData.ContainsKey("Discount"))
|
if (hasDiscount)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-secondary">Discount</td>
|
<td class="text-secondary">Discount</td>
|
||||||
<td class="text-end">@Model.AdditionalData["Discount"]</td>
|
<td class="text-end">@discount</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@if (Model.AdditionalData.ContainsKey("Tip"))
|
if (hasTip)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-secondary">Tip</td>
|
<td class="text-secondary">Tip</td>
|
||||||
<td class="text-end">@Model.AdditionalData["Tip"]</td>
|
<td class="text-end">@tip</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
if (hasTotal)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="text-secondary">Total</th>
|
||||||
|
<td class="text-end fw-semibold">@total</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
<td class="text-nowrap text-secondary">Total</td>
|
||||||
|
<td class="text-end fw-semibold">@DisplayFormatter.Currency(Model.Amount, Model.Currency, DisplayFormatter.CurrencyFormat.Symbol)</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@if (Model.Payments?.Any() is true)
|
@if (Model.Payments?.Any() is true)
|
||||||
{
|
{
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||||
|
</tr>
|
||||||
@for (var i = 0; i < Model.Payments.Count; i++)
|
@for (var i = 0; i < Model.Payments.Count; i++)
|
||||||
{
|
{
|
||||||
var payment = Model.Payments[i];
|
var payment = Model.Payments[i];
|
||||||
|
@ -216,7 +256,9 @@
|
||||||
<hr class="w-100 my-0 bg-none"/>
|
<hr class="w-100 my-0 bg-none"/>
|
||||||
</center>
|
</center>
|
||||||
</body>
|
</body>
|
||||||
|
<script src="~/main/utils.js" asp-append-version="true"></script>
|
||||||
<script>
|
<script>
|
||||||
|
formatDateTimes();
|
||||||
window.print();
|
window.print();
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -2,19 +2,6 @@ const baseUrl = Object.values(document.scripts).find(s => s.src.includes('/main/
|
||||||
|
|
||||||
const flatpickrInstances = [];
|
const flatpickrInstances = [];
|
||||||
|
|
||||||
const formatDateTimes = format => {
|
|
||||||
// select only elements which haven't been initialized before, those without data-localized
|
|
||||||
document.querySelectorAll("time[datetime]:not([data-localized])").forEach($el => {
|
|
||||||
const date = new Date($el.getAttribute("datetime"));
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
|
|
||||||
const { dateStyle = 'short', timeStyle = 'short' } = $el.dataset;
|
|
||||||
// initialize and set localized attribute
|
|
||||||
$el.dataset.localized = new Intl.DateTimeFormat('default', { dateStyle, timeStyle }).format(date);
|
|
||||||
// set text to chosen mode
|
|
||||||
const mode = format || $el.dataset.initial;
|
|
||||||
if ($el.dataset[mode]) $el.innerText = $el.dataset[mode];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const switchTimeFormat = event => {
|
const switchTimeFormat = event => {
|
||||||
const curr = event.target.dataset.mode || 'localized';
|
const curr = event.target.dataset.mode || 'localized';
|
||||||
|
@ -166,8 +153,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize timezone offset value if field is present in page
|
// initialize timezone offset value if field is present in page
|
||||||
var timezoneOffset = new Date().getTimezoneOffset();
|
const $timezoneOffset = document.getElementById("TimezoneOffset");
|
||||||
$("#TimezoneOffset").val(timezoneOffset);
|
const timezoneOffset = new Date().getTimezoneOffset();
|
||||||
|
if ($timezoneOffset) $timezoneOffset.value = timezoneOffset;
|
||||||
|
|
||||||
// localize all elements that have localizeDate class
|
// localize all elements that have localizeDate class
|
||||||
formatDateTimes();
|
formatDateTimes();
|
||||||
|
|
|
@ -15,3 +15,17 @@ function debounce(key, fn, delay = 250) {
|
||||||
clearTimeout(DEBOUNCE_TIMERS[key])
|
clearTimeout(DEBOUNCE_TIMERS[key])
|
||||||
DEBOUNCE_TIMERS[key] = setTimeout(fn, delay)
|
DEBOUNCE_TIMERS[key] = setTimeout(fn, delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateTimes(format) {
|
||||||
|
// select only elements which haven't been initialized before, those without data-localized
|
||||||
|
document.querySelectorAll("time[datetime]:not([data-localized])").forEach($el => {
|
||||||
|
const date = new Date($el.getAttribute("datetime"));
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
|
||||||
|
const { dateStyle = 'short', timeStyle = 'short' } = $el.dataset;
|
||||||
|
// initialize and set localized attribute
|
||||||
|
$el.dataset.localized = new Intl.DateTimeFormat('default', { dateStyle, timeStyle }).format(date);
|
||||||
|
// set text to chosen mode
|
||||||
|
const mode = format || $el.dataset.initial;
|
||||||
|
if ($el.dataset[mode]) $el.innerText = $el.dataset[mode];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue