Add ability to accept tips in POS terminal (#2983)

* Add ability to accept tips in POS terminal

* Add logic for showing and hiding sections specific to a POS app type

* Fix issue with floating point error
This commit is contained in:
Umar Bolatov 2021-10-25 03:06:32 -07:00 committed by GitHub
parent 05f99f3855
commit fd27bd94e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 210 additions and 46 deletions

View File

@ -52,51 +52,57 @@
<span class="text-secondary">Choose the point of sale style for your customers.</span>
</div>
</div>
<div class="form-group">
<div class="form-group" id="button-price-text">
<label asp-for="ButtonText" class="form-label" data-required></label>
<input asp-for="ButtonText" class="form-control" required />
<span asp-validation-for="ButtonText" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-4">Discounts</h4>
<div class="form-check mb-4">
<input asp-for="ShowDiscount" type="checkbox" class="form-check-input" />
<label asp-for="ShowDiscount" class="form-check-label"></label>
<span asp-validation-for="ShowDiscount" class="text-danger"></span>
<div class="mt-2">
<span class="text-secondary">Not recommended for customer self-checkout.</span>
<section id="discounts" class="p-0">
<h4 class="mt-5 mb-4">Discounts</h4>
<div class="form-check mb-4">
<input asp-for="ShowDiscount" type="checkbox" class="form-check-input" />
<label asp-for="ShowDiscount" class="form-check-label"></label>
<span asp-validation-for="ShowDiscount" class="text-danger"></span>
<div class="mt-2">
<span class="text-secondary">Not recommended for customer self-checkout.</span>
</div>
</div>
</div>
<h4 class="mt-5 mb-4">Custom Payments</h4>
<div class="form-group mb-4 d-flex align-items-center">
<input asp-for="ShowCustomAmount" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#CustomAmountSettings" aria-expanded="@Model.ShowCustomAmount" aria-controls="CustomAmountSettings"/>
<label asp-for="ShowCustomAmount" class="form-label mb-0"></label>
<span asp-validation-for="ShowCustomAmount" class="text-danger"></span>
</div>
<div class="collapse @(Model.ShowCustomAmount ? "show" : "")" id="CustomAmountSettings">
<div class="form-group mb-0 pb-3">
<label asp-for="CustomButtonText" class="form-label" data-required></label>
<input asp-for="CustomButtonText" class="form-control" required />
<span asp-validation-for="CustomButtonText" class="text-danger"></span>
</section>
<section id="custom-payments" class="p-0">
<h4 class="mt-5 mb-4">Custom Payments</h4>
<div class="form-group mb-4 d-flex align-items-center">
<input asp-for="ShowCustomAmount" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#CustomAmountSettings" aria-expanded="@Model.ShowCustomAmount" aria-controls="CustomAmountSettings"/>
<label asp-for="ShowCustomAmount" class="form-label mb-0"></label>
<span asp-validation-for="ShowCustomAmount" class="text-danger"></span>
</div>
</div>
<h4 class="mt-5 mb-4">Tips</h4>
<div class="form-group mb-4 d-flex align-items-center">
<input asp-for="EnableTips" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#CustomTipsSettings" aria-expanded="@Model.EnableTips" aria-controls="CustomTipsSettings" />
<label asp-for="EnableTips" class="form-label mb-0"></label>
<span asp-validation-for="EnableTips" class="text-danger"></span>
</div>
<div class="collapse @(Model.EnableTips ? "show" : "")" id="CustomTipsSettings">
<div class="form-group">
<label asp-for="CustomTipText" class="form-label" data-required></label>
<input asp-for="CustomTipText" class="form-control" required />
<span asp-validation-for="CustomTipText" class="text-danger"></span>
<div class="collapse @(Model.ShowCustomAmount ? "show" : "")" id="CustomAmountSettings">
<div class="form-group mb-0 pb-3">
<label asp-for="CustomButtonText" class="form-label" data-required></label>
<input asp-for="CustomButtonText" class="form-control" required />
<span asp-validation-for="CustomButtonText" class="text-danger"></span>
</div>
</div>
<div class="form-group mb-0 pb-3">
<label asp-for="CustomTipPercentages" class="form-label"></label>
<input asp-for="CustomTipPercentages" class="form-control" />
<span asp-validation-for="CustomTipPercentages" class="text-danger"></span>
</section>
<section id="tips" class="p-0">
<h4 class="mt-5 mb-4">Tips</h4>
<div class="form-group mb-4 d-flex align-items-center">
<input asp-for="EnableTips" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#CustomTipsSettings" aria-expanded="@Model.EnableTips" aria-controls="CustomTipsSettings" />
<label asp-for="EnableTips" class="form-label mb-0"></label>
<span asp-validation-for="EnableTips" class="text-danger"></span>
</div>
</div>
<div class="collapse @(Model.EnableTips ? "show" : "")" id="CustomTipsSettings">
<div class="form-group">
<label asp-for="CustomTipText" class="form-label" data-required></label>
<input asp-for="CustomTipText" class="form-control" required />
<span asp-validation-for="CustomTipText" class="text-danger"></span>
</div>
<div class="form-group mb-0 pb-3">
<label asp-for="CustomTipPercentages" class="form-label"></label>
<input asp-for="CustomTipPercentages" class="form-control" />
<span asp-validation-for="CustomTipPercentages" class="text-danger"></span>
</div>
</div>
</section>
</div>
<div class="col-lg-9">
<h4 class="mt-5 mb-4">Additional Options</h4>
@ -303,6 +309,103 @@
</div>
</div>
</script>
<script>
var posStyleSelector = document.getElementById('DefaultView');
posStyleSelector.addEventListener('change', function(e) {
handleStyleSelected(e.target.value);
});
function handleStyleSelected(style) {
switch (style) {
case '0': // Item list only
hideDiscountsSection();
hideButtonPriceTextSection();
showCustomPaymentAmountSection();
hideTipsSection();
break;
case '1': // Item list and cart
showDiscountsSection();
showButtonPriceTextSection();
showCustomPaymentAmountSection();
showTipsSection();
break;
case '2': // Keypad only
hideDiscountsSection();
hideButtonPriceTextSection();
hideCustomPaymentAmountSection();
showTipsSection();
break;
}
}
/** Show/hide discounts section */
var discountsSection = document.getElementById('discounts');
function hideDiscountsSection() {
hideElement(discountsSection);
}
function showDiscountsSection() {
showElement(discountsSection);
}
/***************************************/
/** Show/hide button text section */
var buttonPriceTextSection = document.getElementById('button-price-text');
function hideButtonPriceTextSection() {
hideElement(buttonPriceTextSection);
}
function showButtonPriceTextSection() {
showElement(buttonPriceTextSection);
}
/***************************************/
/** Show/hide custom payments amount seciton */
var customPaymentAmountSection = document.getElementById('custom-payments');
function hideCustomPaymentAmountSection() {
hideElement(customPaymentAmountSection);
}
function showCustomPaymentAmountSection() {
showElement(customPaymentAmountSection);
}
/***************************************/
/** Show/hide tips seciton */
var tipsSection = document.getElementById('tips');
function hideTipsSection() {
hideElement(tipsSection);
}
function showTipsSection() {
showElement(tipsSection);
}
/***************************************/
function hideElement(el) {
el.setAttribute('hidden', true);
}
function showElement(el) {
el.removeAttribute('hidden');
}
document.addEventListener("DOMContentLoaded", function() {
handleStyleSelected(posStyleSelector.value);
});
</script>
<bundle name="wwwroot/bundles/pos-admin-bundle.min.js" asp-append-version="true"></bundle>
}

View File

@ -4,7 +4,6 @@
<div class="l-pos-header bg-primary py-3 px-3">
@if (!string.IsNullOrEmpty(Model.CustomLogoLink))
{
<img src="@Model.CustomLogoLink" height="40"/>
}
else
@ -12,19 +11,19 @@
<h1 class="mb-0">@Model.Title</h1>
}
</div>
<div ref="display" class="l-pos-display pt-5 pb-3 px-3"><div class="text-muted">{{srvModel.currencyCode}}</div><span ref="amount" v-bind:style="{fontSize: fontSize + 'px'}">{{ payTotal }}</span></div>
<div class="l-pos-keypad">
<template
v-for="(key, index) in keys"
:key="index">
<div v-if="key !== ''" class="btn"
v-bind:class="{ 'btn-primary' : (isNaN(key) === false) || key === '.', 'btn-dark' : isNaN(key) && key !== '.' }"
<div v-if="key !== ''" class="btn"
v-bind:class="{ 'btn-primary' : (isNaN(key) === false) || key === '.', 'btn-dark' : isNaN(key) && key !== '.' }"
v-on:click="buttonClicked(key)">{{ key }}</div>
<div v-else class="btn btn-empty"></div>
</template>
</div>
<div class="l-pos-controls mt-2">
<div class="btn btn-outline-secondary btn-lg mb-0" v-on:click="clearTotal">Clear</div><button class="btn btn-primary btn-lg mb-0" type="submit"><b>Pay</b></button>
</div>
@ -32,6 +31,48 @@
<input class="form-control" type="hidden" name="amount" v-model="payTotalNumeric">
</form>
@if (Model.EnableTips)
{
<p class="pt-5 h5">@Model.CustomTipText</p>
<div class="input-group mb-2">
<span class="input-group-text"><i class="fa fa-money fa-fw"></i></span>
<input
class="js-cart-tip form-control form-control-lg"
disabled
type="number"
min="0"
step="@Model.Step"
v-model="tipTotal"
name="tip"
placeholder="Tip in @(Model.CurrencyInfo.CurrencySymbol ?? Model.CurrencyCode)"
>
<a
class="js-cart-tip-remove btn btn-lg btn-danger"
href="#"
v-on:click="removeTip"
><i class="fa fa-times"></i></a>
</div>
<div class="row">
@if (Model.CustomTipPercentages != null && Model.CustomTipPercentages.Length > 0)
{
@for (int i = 0; i < Model.CustomTipPercentages.Length; i++)
{
var percentage = Model.CustomTipPercentages[i];
<div class="@(Model.CustomTipPercentages.Length > 3 ? "col-4" : "col")">
<button
class="js-cart-tip-btn btn btn-lg btn-light w-100 border mb-2"
data-tip="@percentage"
v-on:click="tipClicked(@percentage)"
>
@percentage%
</button>
</div>
}
}
</div>
}
<div class="text-center mt-4 mb-3 py-2">
<svg class="logo" viewBox="0 0 192 84" xmlns="http://www.w3.org/2000/svg"><g><path d="M5.206 83.433a4.86 4.86 0 01-4.859-4.861V5.431a4.86 4.86 0 119.719 0v73.141a4.861 4.861 0 01-4.86 4.861" fill="#CEDC21" class="logo-brand-light"/><path d="M5.209 83.433a4.862 4.862 0 01-2.086-9.253L32.43 60.274 2.323 38.093a4.861 4.861 0 015.766-7.826l36.647 26.999a4.864 4.864 0 01-.799 8.306L7.289 82.964a4.866 4.866 0 01-2.08.469" fill="#51B13E" class="logo-brand-medium"/><path d="M5.211 54.684a4.86 4.86 0 01-2.887-8.774L32.43 23.73 3.123 9.821a4.861 4.861 0 014.166-8.784l36.648 17.394a4.86 4.86 0 01.799 8.305l-36.647 27a4.844 4.844 0 01-2.878.948" fill="#CEDC21" class="logo-brand-light"/><path d="M10.066 31.725v20.553L24.01 42.006z" fill="#1E7A44" class="logo-brand-dark"/><path d="M10.066 5.431A4.861 4.861 0 005.206.57 4.86 4.86 0 00.347 5.431v61.165h9.72V5.431h-.001z" fill="#CEDC21" class="logo-brand-light"/><path d="M74.355 41.412c3.114.884 4.84 3.704 4.84 7.238 0 5.513-3.368 8.082-7.955 8.082H60.761V27.271h9.259c4.504 0 7.997 2.146 7.997 7.743 0 2.821-1.179 5.43-3.662 6.398m-4.293-.716c3.324 0 6.018-1.179 6.018-5.724 0-4.586-2.776-5.808-6.145-5.808h-7.197v11.531h7.324v.001zm1.052 14.099c3.366 0 6.06-1.768 6.06-6.145 0-4.713-3.072-6.144-6.901-6.144h-7.534v12.288h8.375v.001zM98.893 27.271v1.81h-8.122v27.651h-1.979V29.081h-8.123v-1.81zM112.738 26.85c5.01 0 9.554 2.524 10.987 8.543h-1.895c-1.348-4.923-5.303-6.732-9.134-6.732-6.944 0-10.605 5.681-10.605 13.341 0 8.08 3.661 13.256 10.646 13.256 4.125 0 7.828-1.85 9.26-7.279h1.895c-1.264 6.271-6.229 9.174-11.154 9.174-7.87 0-12.583-5.808-12.583-15.15 0-8.966 4.969-15.153 12.583-15.153M138.709 27.271c5.091 0 8.795 3.326 8.795 9.764 0 6.06-3.704 9.722-8.795 9.722h-7.746v9.976h-1.935V27.271h9.681zm0 17.549c3.745 0 6.816-2.397 6.816-7.827 0-5.429-2.947-7.869-6.816-7.869h-7.746V44.82h7.746zM147.841 56.732v-.255l11.741-29.29h.885l11.615 29.29v.255h-2.062l-3.322-8.501H153.27l-3.324 8.501h-2.105zm12.164-26.052l-6.059 15.697h12.078l-6.019-15.697zM189.551 27.271h2.104v.293l-9.176 16.92v12.248h-2.02V44.484l-9.216-16.961v-.252h2.147l3.997 7.492 4.043 7.786h.04l4.081-7.786z" class="logo-brand-text"/></g></svg>
</div>

View File

@ -11,6 +11,8 @@ document.addEventListener("DOMContentLoaded",function (ev) {
srvModel: window.srvModel,
payTotal: '0',
payTotalNumeric: 0,
tipTotal: null,
tipTotalNumeric: 0,
fontSize: displayFontSize,
defaultFontSize: displayFontSize,
keys: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '0', 'C']
@ -52,6 +54,8 @@ document.addEventListener("DOMContentLoaded",function (ev) {
clearTotal: function() {
this.payTotal = '0';
this.payTotalNumeric = 0;
this.tipTotal = null;
this.tipTotalNumeric = 0;
},
buttonClicked: function(key) {
var payTotal = this.payTotal;
@ -70,17 +74,33 @@ document.addEventListener("DOMContentLoaded",function (ev) {
}
payTotal += key;
var divsibility = this.srvModel.currencyInfo.divisibility;
var divisibility = this.srvModel.currencyInfo.divisibility;
var decimalIndex = payTotal.indexOf('.')
if (decimalIndex !== -1 && (payTotal.length - decimalIndex-1 > divsibility)) {
if (decimalIndex !== -1 && (payTotal.length - decimalIndex - 1 > divisibility)) {
payTotal = payTotal.replace(".", "");
payTotal = payTotal.substr(0, payTotal.length - divsibility) + "." + payTotal.substr(payTotal.length - divsibility);
payTotal = payTotal.substr(0, payTotal.length - divisibility) + "." + payTotal.substr(payTotal.length - divisibility);
}
}
this.payTotal = payTotal;
this.payTotalNumeric = parseFloat(payTotal);
}
this.tipTotalNumeric = 0;
this.tipTotal = null;
},
tipClicked: function(percentage) {
this.payTotalNumeric -= this.tipTotalNumeric;
this.tipTotalNumeric = parseFloat((this.payTotalNumeric * (percentage / 100)).toFixed(this.srvModel.currencyInfo.divisibility));
this.payTotalNumeric = parseFloat((this.payTotalNumeric + this.tipTotalNumeric).toFixed(this.srvModel.currencyInfo.divisibility));
this.payTotal = this.payTotalNumeric.toString(10);
this.tipTotal = this.tipTotalNumeric === 0 ? null : this.tipTotalNumeric.toFixed(this.srvModel.currencyInfo.divisibility);
},
removeTip: function() {
this.payTotalNumeric -= this.tipTotalNumeric;
this.payTotal = this.payTotalNumeric.toString(10);
this.tipTotalNumeric = 0;
this.tipTotal = null;
},
}
});
});