Keypad: List recent transactions (#5478)

* Keypad: List recent transactions

Closes #5379.

* UI updates

* Optional: No border

* Fix class

* Decrease keypad max-width
This commit is contained in:
d11n 2023-11-30 10:19:03 +01:00 committed by GitHub
parent b9b3860e6b
commit bdf56c0a6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 134 additions and 13 deletions

View file

@ -47,6 +47,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
AppService appService, AppService appService,
CurrencyNameTable currencies, CurrencyNameTable currencies,
StoreRepository storeRepository, StoreRepository storeRepository,
InvoiceRepository invoiceRepository,
UIInvoiceController invoiceController, UIInvoiceController invoiceController,
FormDataService formDataService, FormDataService formDataService,
DisplayFormatter displayFormatter) DisplayFormatter displayFormatter)
@ -54,12 +55,14 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
_currencies = currencies; _currencies = currencies;
_appService = appService; _appService = appService;
_storeRepository = storeRepository; _storeRepository = storeRepository;
_invoiceRepository = invoiceRepository;
_invoiceController = invoiceController; _invoiceController = invoiceController;
_displayFormatter = displayFormatter; _displayFormatter = displayFormatter;
FormDataService = formDataService; FormDataService = formDataService;
} }
private readonly CurrencyNameTable _currencies; private readonly CurrencyNameTable _currencies;
private readonly InvoiceRepository _invoiceRepository;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly AppService _appService; private readonly AppService _appService;
private readonly UIInvoiceController _invoiceController; private readonly UIInvoiceController _invoiceController;
@ -520,6 +523,37 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
viewModel.FormParameters = formParameters; viewModel.FormParameters = formParameters;
return View("Views/UIForms/View", viewModel); return View("Views/UIForms/View", viewModel);
} }
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("/apps/{appId}/pos/recent-transactions")]
public async Task<IActionResult> RecentTransactions(string appId)
{
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
if (app == null)
return NotFound();
var from = DateTimeOffset.UtcNow - TimeSpan.FromDays(3);
var invoices = await AppService.GetInvoicesForApp(_invoiceRepository, app, from, new[]
{
InvoiceState.ToString(InvoiceStatusLegacy.New),
InvoiceState.ToString(InvoiceStatusLegacy.Paid),
InvoiceState.ToString(InvoiceStatusLegacy.Confirmed),
InvoiceState.ToString(InvoiceStatusLegacy.Complete),
InvoiceState.ToString(InvoiceStatusLegacy.Expired),
InvoiceState.ToString(InvoiceStatusLegacy.Invalid)
});
var recent = invoices
.Take(10)
.Select(i => new JObject
{
["id"] = i.Id,
["date"] = i.InvoiceTime,
["price"] = _displayFormatter.Currency(i.Price, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
["status"] = i.GetInvoiceState().Status.ToModernStatus().ToString(),
["url"] = Url.Action(nameof(UIInvoiceController.Invoice), "UIInvoice", new { invoiceId = i.Id })
});
return Json(recent);
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("{appId}/settings/pos")] [HttpGet("{appId}/settings/pos")]

View file

@ -51,10 +51,45 @@
<div class="keypad"> <div class="keypad">
<button v-for="k in keys" :key="k" :disabled="k === '+' && mode !== 'amounts'" v-on:click.prevent="keyPressed(k)" v-on:dblclick.prevent="doubleClick(k)" type="button" class="btn btn-secondary btn-lg" :data-key="k">{{ k }}</button> <button v-for="k in keys" :key="k" :disabled="k === '+' && mode !== 'amounts'" v-on:click.prevent="keyPressed(k)" v-on:dblclick.prevent="doubleClick(k)" type="button" class="btn btn-secondary btn-lg" :data-key="k">{{ k }}</button>
</div> </div>
<button class="btn btn-lg btn-primary mx-3" type="submit" :disabled="payButtonLoading" id="pay-button"> <button class="btn btn-lg btn-primary mx-3" type="submit" :disabled="payButtonLoading || totalNumeric <= 0" id="pay-button">
<div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status"> <div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
<template v-else>Charge</template> <template v-else>Charge</template>
</button> </button>
<div class="modal" tabindex="-1" id="RecentTransactions" ref="RecentTransactions" data-bs-backdrop="static" data-url="@Url.Action("RecentTransactions", "UIPointOfSale", new { appId = Model.AppId })">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Recent Transactions</h5>
<button type="button" class="btn btn-link px-3 py-0" aria-label="Refresh" v-on:click="loadRecentTransactions" :disabled="recentTransactionsLoading" id="RecentTransactionsRefresh">
<vc:icon symbol="refresh"/>
<span v-if="recentTransactionsLoading" class="visually-hidden">Loading...</span>
</button>
<button type="button" class="btn-close py-3" aria-label="Close" v-on:click="closeModal">
<vc:icon symbol="close"/>
</button>
</div>
<div class="modal-body">
<div v-if="recentTransactions.length" class="list-group list-group-flush">
<a v-for="t in recentTransactions" :key="t.id" :href="t.url" class="list-group-item list-group-item-action d-flex align-items-center gap-3 pe-1 py-3">
<div class="d-flex align-items-baseline justify-content-between flex-wrap flex-grow-1 gap-2">
<span class="flex-grow-1">{{displayDate(t.date)}}</span>
<span class="flex-grow-1 text-end">{{t.price}}</span>
<div class="badge-container">
<span class="badge" :class="`badge-${t.status.toLowerCase()}`">{{t.status}}</span>
</div>
</div>
<vc:icon symbol="caret-right" />
</a>
</div>
<p v-else-if="recentTransactionsLoading" class="text-muted my-0">Loading...</p>
<p v-else class="text-muted my-0">No transactions, yet.</p>
</div>
</div>
</div>
</div>
<button type="button" class="btn btn-link p-1" data-bs-toggle="modal" data-bs-target="#RecentTransactions" id="RecentTransactionsToggle">
<vc:icon symbol="manage-plugins"/>
</button>
</form> </form>

View file

@ -63,6 +63,7 @@
<symbol id="pos-print" viewBox="0 0 24 24" fill="none"><path d="M5 6v13.543l2.333-.914L9.667 20 12 18.629 14.333 20l2.334-1.371 2.333.914V6a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2Z" stroke="currentColor" stroke-width="1.6"/><path d="M8.5 8h7M8.5 11.5h7M8.5 15h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol> <symbol id="pos-print" viewBox="0 0 24 24" fill="none"><path d="M5 6v13.543l2.333-.914L9.667 20 12 18.629 14.333 20l2.334-1.371 2.333.914V6a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2Z" stroke="currentColor" stroke-width="1.6"/><path d="M8.5 8h7M8.5 11.5h7M8.5 15h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol>
<symbol id="pos-static" viewBox="0 0 24 24" fill="none"><path d="M19.05 10.266v6.265a2.244 2.244 0 0 1-2.238 2.238H7.246a2.244 2.244 0 0 1-2.238-2.238v-6.265" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10"/><path d="M9.455 5.262v3.833c0 1.174-.951 2.153-2.126 2.153h-.42a2.613 2.613 0 0 1-2.405-3.664l.56-1.315A1.702 1.702 0 0 1 6.63 5.234l2.825.028ZM14.547 5.258V9.09c0 1.175.95 2.154 2.126 2.154h.42c1.901 0 3.16-1.93 2.405-3.665l-.56-1.314a1.721 1.721 0 0 0-1.566-1.007h-2.825ZM12.002 11.499A2.525 2.525 0 0 1 9.484 8.98V5.29h5.063v3.692c0 1.399-1.147 2.518-2.545 2.518Z" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10"/></symbol> <symbol id="pos-static" viewBox="0 0 24 24" fill="none"><path d="M19.05 10.266v6.265a2.244 2.244 0 0 1-2.238 2.238H7.246a2.244 2.244 0 0 1-2.238-2.238v-6.265" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10"/><path d="M9.455 5.262v3.833c0 1.174-.951 2.153-2.126 2.153h-.42a2.613 2.613 0 0 1-2.405-3.664l.56-1.315A1.702 1.702 0 0 1 6.63 5.234l2.825.028ZM14.547 5.258V9.09c0 1.175.95 2.154 2.126 2.154h.42c1.901 0 3.16-1.93 2.405-3.665l-.56-1.314a1.721 1.721 0 0 0-1.566-1.007h-2.825ZM12.002 11.499A2.525 2.525 0 0 1 9.484 8.98V5.29h5.063v3.692c0 1.399-1.147 2.518-2.545 2.518Z" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10"/></symbol>
<symbol id="pull-payments" viewBox="0 0 24 24" fill="none"><path d="M12 20a8 8 0 1 1 0-16 8 8 0 0 1 0 16Zm0-15.19a7.2 7.2 0 0 0 0 14.38A7.2 7.2 0 0 0 12 4.8Z" fill="currentColor" stroke="currentColor" stroke-width=".6"/><path d="M9.48 14.85a.44.44 0 0 1-.3-.14c-.14-.16-.14-.43.05-.57l5.02-4.31c.16-.14.43-.14.57.05.14.17.14.44-.05.57l-5.05 4.29c-.05.08-.16.1-.24.1Z" fill="currentColor" stroke="currentColor" stroke-width=".6"/><path d="M14.39 14.28a.4.4 0 0 1-.41-.4l.1-3.42-3.08-.17a.4.4 0 0 1-.38-.43c0-.22.19-.4.43-.38l3.47.19c.22 0 .38.19.38.4l-.13 3.83c.02.19-.17.38-.38.38Z" fill="currentColor" stroke="currentColor" stroke-width=".6"/></symbol> <symbol id="pull-payments" viewBox="0 0 24 24" fill="none"><path d="M12 20a8 8 0 1 1 0-16 8 8 0 0 1 0 16Zm0-15.19a7.2 7.2 0 0 0 0 14.38A7.2 7.2 0 0 0 12 4.8Z" fill="currentColor" stroke="currentColor" stroke-width=".6"/><path d="M9.48 14.85a.44.44 0 0 1-.3-.14c-.14-.16-.14-.43.05-.57l5.02-4.31c.16-.14.43-.14.57.05.14.17.14.44-.05.57l-5.05 4.29c-.05.08-.16.1-.24.1Z" fill="currentColor" stroke="currentColor" stroke-width=".6"/><path d="M14.39 14.28a.4.4 0 0 1-.41-.4l.1-3.42-3.08-.17a.4.4 0 0 1-.38-.43c0-.22.19-.4.43-.38l3.47.19c.22 0 .38.19.38.4l-.13 3.83c.02.19-.17.38-.38.38Z" fill="currentColor" stroke="currentColor" stroke-width=".6"/></symbol>
<symbol id="refresh" viewBox="0 0 24 24" fill="none"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></g></symbol>
<symbol id="remove" viewBox="0 0 24 24" fill="none"><path d="M17 11H13V7C13 6.45 12.55 6 12 6C11.45 6 11 6.45 11 7V11H7C6.45 11 6 11.45 6 12C6 12.55 6.45 13 7 13H11V17C11 17.55 11.45 18 12 18C12.55 18 13 17.55 13 17V13H17C17.55 13 18 12.55 18 12C18 11.45 17.55 11 17 11Z" fill="currentColor" transform="rotate(45 12 12)"/></symbol> <symbol id="remove" viewBox="0 0 24 24" fill="none"><path d="M17 11H13V7C13 6.45 12.55 6 12 6C11.45 6 11 6.45 11 7V11H7C6.45 11 6 11.45 6 12C6 12.55 6.45 13 7 13H11V17C11 17.55 11.45 18 12 18C12.55 18 13 17.55 13 17V13H17C17.55 13 18 12.55 18 12C18 11.45 17.55 11 17 11Z" fill="currentColor" transform="rotate(45 12 12)"/></symbol>
<symbol id="scan-qr" viewBox="0 0 32 32" fill="none"><path d="M20 .875h10c.621 0 1.125.504 1.125 1.125v10m0 8v10c0 .621-.504 1.125-1.125 1.125H20m-8 0H2A1.125 1.125 0 01.875 30V20m0-8V2C.875 1.379 1.379.875 2 .875h10" stroke="currentColor" stroke-width="1.75" fill="none" fill-rule="evenodd"/></symbol> <symbol id="scan-qr" viewBox="0 0 32 32" fill="none"><path d="M20 .875h10c.621 0 1.125.504 1.125 1.125v10m0 8v10c0 .621-.504 1.125-1.125 1.125H20m-8 0H2A1.125 1.125 0 01.875 30V20m0-8V2C.875 1.379 1.379.875 2 .875h10" stroke="currentColor" stroke-width="1.75" fill="none" fill-rule="evenodd"/></symbol>
<symbol id="search" viewBox="0 0 24 24" fill="none"><circle cx="11.5" cy="11.75" r="6.25" stroke="currentColor" stroke-width="1.5"/><path d="M16.5 16.25L19.5 19.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol> <symbol id="search" viewBox="0 0 24 24" fill="none"><circle cx="11.5" cy="11.75" r="6.25" stroke="currentColor" stroke-width="1.5"/><path d="M16.5 16.25L19.5 19.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol>

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View file

@ -205,7 +205,7 @@ document.addEventListener("DOMContentLoaded",function () {
window.addEventListener('pagehide', () => { window.addEventListener('pagehide', () => {
if (this.payButtonLoading) { if (this.payButtonLoading) {
this.unsetPayButtonLoading(); this.payButtonLoading = false;
localStorage.removeItem(storageKey('cart')); localStorage.removeItem(storageKey('cart'));
} }
}) })

View file

@ -85,9 +85,6 @@ const posCommon = {
? percentage ? percentage
: null; : null;
}, },
unsetPayButtonLoading () {
this.payButtonLoading = false
},
formatCrypto (value, withSymbol) { formatCrypto (value, withSymbol) {
const symbol = withSymbol ? ` ${this.currencySymbol || this.currencyCode}` : '' const symbol = withSymbol ? ` ${this.currencySymbol || this.currencyCode}` : ''
const { divisibility } = this.currencyInfo const { divisibility } = this.currencyInfo

View file

@ -1,5 +1,40 @@
#PosKeypad { #PosKeypad {
--wrap-max-width: 575px; --wrap-max-width: 500px;
overflow: hidden;
position: relative;
}
#RecentTransactionsToggle {
position: absolute;
top: var(--btcpay-space-l);
left: var(--btcpay-space-m);
z-index: 1;
}
#RecentTransactionsToggle .icon {
--btn-icon-size: 2.25em;
}
#RecentTransactionsRefresh[disabled] .icon {
animation: 1.25s linear infinite spinner-border;
}
#RecentTransactions .list-group {
margin: calc(var(--btcpay-modal-padding) * -1);
width: calc(100% + var(--btcpay-modal-padding) * 2);
}
#RecentTransactions .list-group-item {
background-color: transparent;
}
#RecentTransactions .list-group .badge-container {
flex: 0 0 5.125rem;
text-align: right;
}
@media (max-width: 359px) {
#RecentTransactions .list-group .badge-container {
flex-grow: 1;
}
} }
/* modes */ /* modes */
@ -19,6 +54,7 @@
padding: 0; padding: 0;
position: relative; position: relative;
border-radius: 0; border-radius: 0;
border-color: transparent !important;
font-weight: var(--btcpay-font-weight-semibold); font-weight: var(--btcpay-font-weight-semibold);
font-size: 24px; font-size: 24px;
min-height: 3.5rem; min-height: 3.5rem;
@ -79,7 +115,6 @@
} }
/* fix sticky hover effect on mobile browsers */ /* fix sticky hover effect on mobile browsers */
@media (hover: none) { @media (hover: none) {
.keypad .btn-secondary:hover,
.actions .btn-secondary:hover { .actions .btn-secondary:hover {
border-color: var(--btcpay-secondary-border-active) !important; border-color: var(--btcpay-secondary-border-active) !important;
} }

View file

@ -9,7 +9,10 @@ document.addEventListener("DOMContentLoaded",function () {
fontSize: displayFontSize, fontSize: displayFontSize,
defaultFontSize: displayFontSize, defaultFontSize: displayFontSize,
keys: ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'C', '0', '+'], keys: ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'C', '0', '+'],
amounts: [null] amounts: [null],
recentTransactions: [],
recentTransactionsLoading: false,
dateFormatter: new Intl.DateTimeFormat('default', { dateStyle: 'short', timeStyle: 'short' })
} }
}, },
computed: { computed: {
@ -117,15 +120,31 @@ document.addEventListener("DOMContentLoaded",function () {
// clear completely // clear completely
this.clear(); this.clear();
} }
},
closeModal() {
bootstrap.Modal.getInstance(this.$refs.RecentTransactions).hide();
},
displayDate(val) {
const date = new Date(val);
return this.dateFormatter.format(date);
},
async loadRecentTransactions() {
this.recentTransactionsLoading = true;
const { url } = this.$refs.RecentTransactions.dataset;
const response = await fetch(url);
if (response.ok) {
this.recentTransactions = await response.json();
}
this.recentTransactionsLoading = false;
} }
}, },
created() { created() {
/** We need to unset state in case user clicks the browser back button */ // We need to unset state in case user clicks the browser back button
window.addEventListener('pagehide', this.unsetPayButtonLoading) window.addEventListener('pagehide', () => { this.payButtonLoading = false })
},
destroyed() {
window.removeEventListener('pagehide', this.unsetPayButtonLoading)
}, },
mounted() {
this.$refs.RecentTransactions.addEventListener('show.bs.modal', this.loadRecentTransactions);
}
}); });
}); });