2024-03-14 11:11:54 +01:00
|
|
|
@using BTCPayServer.Plugins.PointOfSale.Models
|
|
|
|
@using BTCPayServer.Services
|
|
|
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
|
|
|
@using Newtonsoft.Json.Linq
|
2023-12-04 14:14:37 +01:00
|
|
|
@using BTCPayServer.Client
|
2024-03-14 11:11:54 +01:00
|
|
|
@inject DisplayFormatter DisplayFormatter
|
2022-07-18 20:51:53 +02:00
|
|
|
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
2024-03-14 11:11:54 +01:00
|
|
|
@functions {
|
|
|
|
private string GetItemPriceFormatted(ViewPointOfSaleViewModel.Item item)
|
|
|
|
{
|
|
|
|
if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup) return "any amount";
|
|
|
|
if (item.Price == 0) return "free";
|
|
|
|
var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol);
|
|
|
|
return item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum ? $"{formatted} minimum" : formatted;
|
|
|
|
}
|
|
|
|
}
|
2023-11-21 10:13:26 +01:00
|
|
|
<form id="app" 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>
|
2024-03-14 11:11:54 +01:00
|
|
|
<input type="hidden" name="amount" :value="totalNumeric">
|
|
|
|
<input type="hidden" name="tip" :value="tipNumeric">
|
|
|
|
<input type="hidden" name="discount" :value="discountPercentNumeric">
|
|
|
|
<input type="hidden" name="posdata" :value="posdata">
|
2023-02-10 16:26:38 +01:00
|
|
|
<div ref="display" class="d-flex flex-column align-items-center px-4 mb-auto">
|
2023-07-22 14:15:41 +02:00
|
|
|
<div class="fw-semibold text-muted" id="Currency">{{currencyCode}}</div>
|
2023-06-22 08:57:29 +02:00
|
|
|
<div class="fw-bold lh-sm" ref="amount" v-bind:style="{ fontSize: `${fontSize}px` }" id="Amount">{{ formatCurrency(total, false) }}</div>
|
2023-11-02 20:03:34 +01:00
|
|
|
<div class="text-muted text-center mt-2" id="Calculation">{{ calculation }}</div>
|
2023-02-10 16:26:38 +01:00
|
|
|
</div>
|
2023-07-22 14:15:41 +02:00
|
|
|
<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">
|
2023-06-22 08:57:29 +02:00
|
|
|
<div class="h4 fw-semibold text-muted text-center" id="Discount">
|
2023-02-10 16:26:38 +01:00
|
|
|
<span class="h3 text-body me-1">{{discountPercent || 0}}%</span> discount
|
|
|
|
</div>
|
2020-09-21 14:06:31 +08:00
|
|
|
</div>
|
2023-07-22 14:15:41 +02:00
|
|
|
<div id="Mode-Tip" class="tab-pane fade px-2" :class="{ show: mode === 'tip', active: mode === 'tip' }" role="tabpanel" aria-labelledby="ModeTablist-Tip" v-if="enableTips">
|
2023-02-10 16:26:38 +01:00
|
|
|
<div class="btcpay-pills d-flex flex-wrap align-items-center justify-content-center gap-2">
|
2023-07-22 14:15:41 +02:00
|
|
|
<template v-if="customTipPercentages">
|
2023-01-30 09:23:49 +01:00
|
|
|
<button
|
2023-06-22 08:57:29 +02:00
|
|
|
id="Tip-Custom"
|
2023-02-10 16:26:38 +01:00
|
|
|
type="button"
|
|
|
|
class="btcpay-pill"
|
|
|
|
:class="{ active: !tipPercent }"
|
|
|
|
v-on:click.prevent="tipPercent = null">
|
|
|
|
<template v-if="tip && tip > 0">{{formatCurrency(tip, true)}}</template>
|
|
|
|
<template v-else>Custom</template>
|
2023-01-30 09:23:49 +01:00
|
|
|
</button>
|
2023-02-10 16:26:38 +01:00
|
|
|
<button
|
2023-07-22 14:15:41 +02:00
|
|
|
v-for="percentage in customTipPercentages"
|
2023-02-10 16:26:38 +01:00
|
|
|
type="button"
|
|
|
|
class="btcpay-pill"
|
|
|
|
:class="{ active: tipPercent == percentage }"
|
2023-06-22 08:57:29 +02:00
|
|
|
:id="`Tip-${percentage}`"
|
2023-02-10 16:26:38 +01:00
|
|
|
v-on:click.prevent="tipPercentage(percentage)">
|
|
|
|
{{ percentage }}%
|
|
|
|
</button>
|
|
|
|
</template>
|
|
|
|
<div v-else class="h5 fw-semibold text-muted text-center">
|
|
|
|
Amount<template v-if="tip">: {{formatCurrency(tip, true)}}</template>
|
|
|
|
</div>
|
|
|
|
</div>
|
2021-10-25 03:06:32 -07:00
|
|
|
</div>
|
2023-02-10 16:26:38 +01:00
|
|
|
</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">
|
2023-11-02 20:03:34 +01:00
|
|
|
<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">
|
2023-02-10 16:26:38 +01:00
|
|
|
<label :for="`ModeTablist-${m.type}`">{{ m.title }}</label>
|
|
|
|
</template>
|
|
|
|
</div>
|
|
|
|
<div class="keypad">
|
2023-11-02 20:03:34 +01:00
|
|
|
<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>
|
2023-02-10 16:26:38 +01:00
|
|
|
</div>
|
2023-11-30 10:19:03 +01:00
|
|
|
<button class="btn btn-lg btn-primary mx-3" type="submit" :disabled="payButtonLoading || totalNumeric <= 0" id="pay-button">
|
2023-11-02 20:03:34 +01:00
|
|
|
<div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status">
|
2023-11-20 11:18:19 +01:00
|
|
|
<span class="visually-hidden">Loading...</span>
|
2023-11-02 20:03:34 +01:00
|
|
|
</div>
|
|
|
|
<template v-else>Charge</template>
|
|
|
|
</button>
|
2024-03-14 11:11:54 +01:00
|
|
|
<partial name="PointOfSale/Public/RecentTransactions" model="Model"/>
|
|
|
|
<button type="button" class="btn btn-link p-1" data-bs-toggle="modal" data-bs-target="#RecentTransactions" id="RecentTransactionsToggle" permission="@Policies.CanViewInvoices">
|
2024-05-20 01:57:46 +02:00
|
|
|
<vc:icon symbol="nav-invoice"/>
|
2024-03-14 11:11:54 +01:00
|
|
|
</button>
|
|
|
|
<button type="button" class="btn btn-link p-1" data-bs-toggle="offcanvas" data-bs-target="#ItemsListOffcanvas" id="ItemsListToggle" aria-controls="ItemsList" v-if="showItems">
|
2024-05-20 01:57:46 +02:00
|
|
|
<vc:icon symbol="nav-mobile-menu"/>
|
2024-03-14 11:11:54 +01:00
|
|
|
</button>
|
|
|
|
<div class="offcanvas offcanvas-end" data-bs-backdrop="static" tabindex="-1" id="ItemsListOffcanvas" aria-labelledby="ItemsListToggle" v-if="showItems">
|
|
|
|
<div class="offcanvas-header flex-wrap p-3">
|
|
|
|
<h5 class="offcanvas-title" id="offcanvasExampleLabel">Products</h5>
|
|
|
|
<button type="button" class="btn btn-sm rounded-pill" :class="{ 'btn-primary': cart.length > 0, 'btn-outline-secondary': cart.length === 0}" data-bs-dismiss="offcanvas" v-text="cart.length > 0 ? 'Apply' : 'Close'"></button>
|
|
|
|
@if (Model.ShowSearch)
|
|
|
|
{
|
|
|
|
<div class="w-100 mt-3">
|
|
|
|
<input id="SearchTerm" class="form-control rounded-pill" placeholder="Search…" v-model="searchTerm" v-if="showSearch">
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
@if (Model.ShowCategories)
|
|
|
|
{
|
|
|
|
<div id="Categories" ref="categories" v-if="showCategories && allCategories" class="w-100 mt-3 btcpay-pills d-flex flex-wrap align-items-center justify-content-center gap-2" :class="{ 'scrollable': categoriesScrollable }">
|
|
|
|
<nav class="btcpay-pills d-flex align-items-center gap-3" ref="categoriesNav">
|
|
|
|
<template v-for="cat in allCategories">
|
|
|
|
<input :id="`Category-${cat.value}`" type="radio" name="category" autocomplete="off" v-model="displayCategory" :value="cat.value">
|
|
|
|
<label :for="`Category-${cat.value}`" class="btcpay-pill text-nowrap">{{ cat.text }}</label>
|
|
|
|
</template>
|
|
|
|
</nav>
|
2023-11-30 10:19:03 +01:00
|
|
|
</div>
|
2024-03-14 11:11:54 +01:00
|
|
|
}
|
|
|
|
</div>
|
|
|
|
<div class="offcanvas-body">
|
|
|
|
<div ref="posItems" id="PosItems">
|
|
|
|
@for (var index = 0; index < Model.Items.Length; index++)
|
|
|
|
{
|
|
|
|
var item = Model.Items[index];
|
|
|
|
var formatted = GetItemPriceFormatted(item);
|
|
|
|
var inStock = item.Inventory is null or > 0;
|
|
|
|
var displayed = item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed && inStock ? "true" : "false";
|
|
|
|
var categories = new JArray(item.Categories ?? new object[] { });
|
|
|
|
<div class="posItem p-3" :class="{ 'posItem--inStock': inStock(@index), 'posItem--displayed': @displayed }" data-index="@index" data-search="@Safe.RawEncode(item.Title + " " + item.Description)" data-categories='@Safe.Json(categories)' v-show="@displayed">
|
|
|
|
<div class="d-flex align-items-start w-100 gap-3">
|
|
|
|
@if (!string.IsNullOrWhiteSpace(item.Image))
|
|
|
|
{
|
2024-03-28 01:01:56 +01:00
|
|
|
<div class="img d-none d-sm-block">
|
2024-03-22 15:16:59 +01:00
|
|
|
<img src="@item.Image" alt="@item.Title" asp-append-version="true" />
|
2024-03-14 11:11:54 +01:00
|
|
|
</div>
|
|
|
|
}
|
|
|
|
<div class="d-flex flex-column gap-2">
|
|
|
|
<h5 class="card-title m-0">@Safe.Raw(item.Title)</h5>
|
|
|
|
<div class="d-flex gap-2 align-items-center">
|
|
|
|
@if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup || item.Price == 0)
|
|
|
|
{
|
|
|
|
<span class="fw-semibold badge text-bg-info">@Safe.Raw(char.ToUpper(formatted[0]) + formatted[1..])</span>
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
<span class="fw-semibold">@Safe.Raw(formatted)</span>
|
|
|
|
}
|
|
|
|
@if (item.Inventory.HasValue)
|
|
|
|
{
|
|
|
|
<span class="badge text-bg-warning inventory" v-text="inventoryText(@index)">
|
|
|
|
@(item.Inventory > 0 ? $"{item.Inventory} left" : "Sold out")
|
|
|
|
</span>
|
|
|
|
}
|
2023-11-30 10:19:03 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
2024-03-28 01:01:56 +01:00
|
|
|
<div class="d-flex align-items-center gap-2 ms-auto quantities">
|
|
|
|
<button type="button" v-on:click="updateQuantity(`@Safe.Raw(item.Id)`, -1, true)" class="btn btn-minus" :disabled="getQuantity(`@Safe.Raw(item.Id)`) <= 0">
|
|
|
|
<span><vc:icon symbol="minus" /></span>
|
2024-03-14 11:11:54 +01:00
|
|
|
</button>
|
2024-03-28 01:01:56 +01:00
|
|
|
<div class="quantity text-center fs-5" style="width:2rem">{{ getQuantity(`@Safe.Raw(item.Id)`) }}</div>
|
|
|
|
<button type="button" v-on:click="updateQuantity(`@Safe.Raw(item.Id)`, +1, true)" class="btn btn-plus" :disabled="!inStock(@index)">
|
|
|
|
<span><vc:icon symbol="plus" /></span>
|
2024-03-14 11:11:54 +01:00
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
2023-11-30 10:19:03 +01:00
|
|
|
</div>
|
2024-03-14 11:11:54 +01:00
|
|
|
}
|
2023-11-30 10:19:03 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2023-02-10 16:26:38 +01:00
|
|
|
</form>
|