mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
POS Keypad: Add plus and change clear functionality (#5396)
Closes #5299.
This commit is contained in:
parent
c16dfb2dcb
commit
696a414e95
5 changed files with 86 additions and 77 deletions
|
@ -2217,7 +2217,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.Contains("EUR", s.Driver.FindElement(By.Id("Currency")).Text);
|
||||
Assert.Contains("0,00", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-amount")).Selected);
|
||||
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-amounts")).Selected);
|
||||
Assert.False(s.Driver.FindElement(By.Id("ModeTablist-discount")).Enabled);
|
||||
Assert.False(s.Driver.FindElement(By.Id("ModeTablist-tip")).Enabled);
|
||||
|
||||
|
@ -2226,13 +2226,17 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='2']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='3']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='4']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='.']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='0']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='0']")).Click();
|
||||
Assert.Equal("1.234,00", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='+']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='5']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='6']")).Click();
|
||||
Assert.Equal("1.234,56", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-discount")).Enabled);
|
||||
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-tip")).Enabled);
|
||||
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
Assert.Equal("1.234,00 € + 0,56 €", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
|
||||
// Discount: 10%
|
||||
s.Driver.FindElement(By.CssSelector("label[for='ModeTablist-discount']")).Click();
|
||||
|
@ -2240,14 +2244,14 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='0']")).Click();
|
||||
Assert.Contains("1.111,10", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Contains("10% discount", s.Driver.FindElement(By.Id("Discount")).Text);
|
||||
Assert.Contains("1.234,56 € - 123,46 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
Assert.Contains("1.234,00 € + 0,56 € - 123,46 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
|
||||
// Tip: 10%
|
||||
s.Driver.FindElement(By.CssSelector("label[for='ModeTablist-tip']")).Click();
|
||||
s.Driver.WaitForElement(By.Id("Tip-Custom"));
|
||||
s.Driver.FindElement(By.Id("Tip-10")).Click();
|
||||
Assert.Contains("1.222,21", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Contains("1.234,56 € - 123,46 € (10%) + 111,11 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
Assert.Contains("1.234,00 € + 0,56 € - 123,46 € (10%) + 111,11 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
|
||||
// Pay
|
||||
s.Driver.FindElement(By.Id("pay-button")).Click();
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
|
||||
<form id="PosKeypad" method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" v-on:submit="handleFormSubmit" class="d-flex flex-column gap-4 my-auto" v-cloak>
|
||||
<input id="posdata" type="hidden" name="posdata" v-model="posdata">
|
||||
<input type="hidden" name="posdata" v-model="posdata" id="posdata">
|
||||
<input type="hidden" name="amount" v-model="totalNumeric">
|
||||
<div ref="display" class="d-flex flex-column align-items-center px-4 mb-auto">
|
||||
<div class="fw-semibold text-muted" id="Currency">{{currencyCode}}</div>
|
||||
<div class="fw-bold lh-sm" ref="amount" v-bind:style="{ fontSize: `${fontSize}px` }" id="Amount">{{ formatCurrency(total, false) }}</div>
|
||||
<div class="text-muted text-center mt-2" id="Calculation" v-if="showDiscount || enableTips">{{ calculation }}</div>
|
||||
<div class="text-muted text-center mt-2" id="Calculation">{{ calculation }}</div>
|
||||
</div>
|
||||
<div id="ModeTabs" class="tab-content mb-n2" v-if="showDiscount || enableTips">
|
||||
<div id="Mode-Discount" class="tab-pane fade px-2" :class="{ show: mode === 'discount', active: mode === 'discount' }" role="tabpanel" aria-labelledby="ModeTablist-Discount" v-if="showDiscount">
|
||||
|
@ -44,24 +44,17 @@
|
|||
</div>
|
||||
<div id="ModeTablist" class="nav btcpay-pills align-items-center justify-content-center mb-n2 pb-1" role="tablist" v-if="modes.length > 1">
|
||||
<template v-for="m in modes" :key="m.value">
|
||||
<input :id="`ModeTablist-${m.type}`" name="mode" :value="m.type" type="radio" role="tab" data-bs-toggle="pill" :data-bs-target="`#Mode-${m.type}`" :disabled="m.type != 'amount' && amountNumeric == 0" :aria-controls="`Mode-${m.type}`" :aria-selected="mode === m.type" :checked="mode === m.type" v-on:click="mode = m.type">
|
||||
<input :id="`ModeTablist-${m.type}`" name="mode" :value="m.type" type="radio" role="tab" data-bs-toggle="pill" :data-bs-target="`#Mode-${m.type}`" :disabled="m.type != 'amounts' && amountNumeric == 0" :aria-controls="`Mode-${m.type}`" :aria-selected="mode === m.type" :checked="mode === m.type" v-on:click="mode = m.type">
|
||||
<label :for="`ModeTablist-${m.type}`">{{ m.title }}</label>
|
||||
</template>
|
||||
</div>
|
||||
<div class="keypad">
|
||||
<button v-for="k in keys" :key="k" v-on:click.prevent="keyPressed(k)" type="button" class="btn btn-secondary btn-lg" :data-key="k">
|
||||
<template v-if="k === 'del'"><vc:icon symbol="caret-right"/></template>
|
||||
<template v-else>{{ k }}</template>
|
||||
</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 class="actions px-4 gap-4">
|
||||
<button class="btn btn-lg btn-secondary" type="reset" v-on:click.prevent="clear">Clear</button>
|
||||
<button class="btn btn-lg btn-primary" type="submit" :disabled="payButtonLoading" id="pay-button">
|
||||
<div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<template v-else>Charge</template>
|
||||
</button>
|
||||
</div>
|
||||
<input class="form-control" type="hidden" name="amount" v-model="totalNumeric">
|
||||
<button class="btn btn-lg btn-primary mx-3" type="submit" :disabled="payButtonLoading" id="pay-button">
|
||||
<div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<template v-else>Charge</template>
|
||||
</button>
|
||||
</form>
|
||||
|
|
|
@ -44,14 +44,6 @@ const posCommon = {
|
|||
totalNumeric () {
|
||||
return parseFloat(parseFloat(this.total).toFixed(this.currencyInfo.divisibility))
|
||||
},
|
||||
calculation () {
|
||||
if (!this.tipNumeric && !this.discountNumeric) return null
|
||||
let calc = this.formatCurrency(this.amountNumeric, true)
|
||||
if (this.discountNumeric > 0) calc += ` - ${this.formatCurrency(this.discountNumeric, true)} (${this.discountPercent}%)`
|
||||
if (this.tipNumeric > 0) calc += ` + ${this.formatCurrency(this.tipNumeric, true)}`
|
||||
if (this.tipPercent) calc += ` (${this.tipPercent}%)`
|
||||
return calc
|
||||
},
|
||||
posdata () {
|
||||
const data = {
|
||||
subTotal: this.amountNumeric,
|
||||
|
|
|
@ -27,9 +27,8 @@
|
|||
max-height: 6rem;
|
||||
color: var(--btcpay-body-text);
|
||||
}
|
||||
.keypad .btn[data-key="del"] svg {
|
||||
--btn-icon-size: 2.25rem;
|
||||
transform: rotate(180deg);
|
||||
.keypad .btn[data-key="+"] {
|
||||
font-size: 2.25em;
|
||||
}
|
||||
.btcpay-pills label,
|
||||
.btn-secondary.rounded-pill {
|
||||
|
@ -57,15 +56,6 @@
|
|||
z-index: 1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.actions .btn {
|
||||
flex: 1 1 50%;
|
||||
}
|
||||
|
||||
#Calculation {
|
||||
min-height: 1.5rem;
|
||||
}
|
||||
|
|
|
@ -5,28 +5,37 @@ document.addEventListener("DOMContentLoaded",function () {
|
|||
mixins: [posCommon],
|
||||
data () {
|
||||
return {
|
||||
mode: 'amount',
|
||||
mode: 'amounts',
|
||||
fontSize: displayFontSize,
|
||||
defaultFontSize: displayFontSize,
|
||||
keys: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '0', 'del']
|
||||
keys: ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'C', '0', '+'],
|
||||
amounts: [null]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
modes () {
|
||||
const modes = [{ title: 'Amount', type: 'amount' }]
|
||||
const modes = [{ title: 'Amount', type: 'amounts' }]
|
||||
if (this.showDiscount) modes.push({ title: 'Discount', type: 'discount' })
|
||||
if (this.enableTips) modes.push({ title: 'Tip', type: 'tip'})
|
||||
return modes
|
||||
},
|
||||
keypadTarget () {
|
||||
switch (this.mode) {
|
||||
case 'amount':
|
||||
return 'amount';
|
||||
case 'amounts':
|
||||
return 'amounts';
|
||||
case 'discount':
|
||||
return 'discountPercent';
|
||||
case 'tip':
|
||||
return 'tip';
|
||||
}
|
||||
},
|
||||
calculation () {
|
||||
if (!this.tipNumeric && !(this.discountNumeric > 0 || this.discountPercentNumeric > 0) && this.amounts.length < 2) return null
|
||||
let calc = this.amounts.map(amt => this.formatCurrency(amt, true)).join(' + ')
|
||||
if (this.discountNumeric > 0 || this.discountPercentNumeric > 0) calc += ` - ${this.formatCurrency(this.discountNumeric, true)} (${this.discountPercent}%)`
|
||||
if (this.tipNumeric > 0) calc += ` + ${this.formatCurrency(this.tipNumeric, true)}`
|
||||
if (this.tipPercent) calc += ` (${this.tipPercent}%)`
|
||||
return calc
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -46,47 +55,68 @@ document.addEventListener("DOMContentLoaded",function () {
|
|||
this.fontSize = Math.min(this.fontSize * gamma, this.defaultFontSize);
|
||||
}
|
||||
});
|
||||
},
|
||||
amounts (values) {
|
||||
this.amount = values.reduce((total, current) => total + parseFloat(current || '0'), 0);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getWidth (el) {
|
||||
getWidth(el) {
|
||||
const styles = window.getComputedStyle(el),
|
||||
width = parseFloat(el.clientWidth),
|
||||
padL = parseFloat(styles.paddingLeft),
|
||||
padR = parseFloat(styles.paddingRight);
|
||||
return width - padL - padR;
|
||||
},
|
||||
clear () {
|
||||
this.amount = this.tip = this.discount = this.tipPercent = this.discountPercent = null;
|
||||
this.mode = 'amount';
|
||||
clear() {
|
||||
this.amounts = [null];
|
||||
this.tip = this.discount = this.tipPercent = this.discountPercent = null;
|
||||
this.mode = 'amounts';
|
||||
},
|
||||
applyKeyToValue (key, value) {
|
||||
if (!value) value = '';
|
||||
if (key === 'del') {
|
||||
value = value.substring(0, value.length - 1);
|
||||
value = value === '' ? '0' : value;
|
||||
} else if (key === '.') {
|
||||
// Only add decimal point if it doesn't exist yet
|
||||
if (value.indexOf('.') === -1) {
|
||||
value += key;
|
||||
}
|
||||
} else { // Is a digit
|
||||
if (!value || value === '0') {
|
||||
value = '';
|
||||
}
|
||||
value += key;
|
||||
const { divisibility } = this.currencyInfo;
|
||||
const decimalIndex = value.indexOf('.')
|
||||
if (decimalIndex !== -1 && (value.length - decimalIndex - 1 > divisibility)) {
|
||||
value = value.replace('.', '');
|
||||
value = value.substr(0, value.length - divisibility) + '.' +
|
||||
value.substr(value.length - divisibility);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
applyKeyToValue(key, value, divisibility) {
|
||||
if (!value || value === '0') value = '';
|
||||
value = (value + key)
|
||||
.replace('.', '')
|
||||
.padStart(divisibility, '0')
|
||||
.replace(new RegExp(`(\\d*)(\\d{${divisibility}})`), '$1.$2');
|
||||
return parseFloat(value).toFixed(divisibility);
|
||||
},
|
||||
keyPressed (key) {
|
||||
this[this.keypadTarget] = this.applyKeyToValue(key, this[this.keypadTarget]);
|
||||
if (this.keypadTarget === 'amounts') {
|
||||
const lastIndex = this.amounts.length - 1;
|
||||
const lastAmount = this.amounts[lastIndex];
|
||||
if (key === 'C') {
|
||||
if (!lastAmount && lastIndex === 0) {
|
||||
// clear completely
|
||||
this.clear();
|
||||
} else if (!lastAmount) {
|
||||
// remove latest value
|
||||
this.amounts.pop();
|
||||
} else {
|
||||
// clear latest value
|
||||
Vue.set(this.amounts, lastIndex, null);
|
||||
}
|
||||
} else if (key === '+' && parseFloat(lastAmount || '0')) {
|
||||
this.amounts.push(null);
|
||||
} else { // Is a digit
|
||||
const { divisibility } = this.currencyInfo;
|
||||
const value = this.applyKeyToValue(key, lastAmount, divisibility);
|
||||
Vue.set(this.amounts, lastIndex, value);
|
||||
}
|
||||
} else {
|
||||
if (key === 'C') {
|
||||
this[this.keypadTarget] = null;
|
||||
} else {
|
||||
const divisibility = this.keypadTarget === 'tip' ? this.currencyInfo.divisibility : 0;
|
||||
this[this.keypadTarget] = this.applyKeyToValue(key, this[this.keypadTarget], divisibility);
|
||||
}
|
||||
}
|
||||
},
|
||||
doubleClick (key) {
|
||||
if (key === 'C') {
|
||||
// clear completely
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
|
Loading…
Add table
Reference in a new issue