btcpayserver/BTCPayServer/Views/Shared/PointOfSale/Public/VueLight.cshtml
d11n ff79a31066
Refactoring: Move AppItem to Client lib and use the class for item list (#6258)
* Refactoring: Move AppItem to Client lib and use the class for item list

This makes it available for the app, which would otherwise have to replicate the model. Also uses the proper class for the item/perk list of the app models.

* Remove unused app item payment methods property

* Do not ignore nullable values in JSON

* Revert to use Newtonsoft types
2024-11-05 11:49:30 +09:00

170 lines
11 KiB
Text

@using BTCPayServer.Plugins.PointOfSale.Models
@using BTCPayServer.Services
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using Newtonsoft.Json.Linq
@using BTCPayServer.Client
@using BTCPayServer.Client.Models
@inject DisplayFormatter DisplayFormatter
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
@functions {
private string GetItemPriceFormatted(AppItem item)
{
if (item.PriceType == AppItemPriceType.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 == AppItemPriceType.Minimum ? $"{formatted} minimum" : formatted;
}
}
<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>
<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">
<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">{{ 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">
<div class="h4 fw-semibold text-muted text-center" id="Discount">
<span class="h3 text-body me-1">{{discountPercent || 0}}%</span> discount
</div>
</div>
<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">
<div class="btcpay-pills d-flex flex-wrap align-items-center justify-content-center gap-2">
<template v-if="customTipPercentages">
<button
id="Tip-Custom"
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>
</button>
<button
v-for="percentage in customTipPercentages"
type="button"
class="btcpay-pill"
:class="{ active: tipPercent == percentage }"
:id="`Tip-${percentage}`"
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>
</div>
</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 != '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" :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">
<template v-if="k === 'C'"><vc:icon symbol="keypad-clear"/></template>
<template v-else-if="k === '+'"><vc:icon symbol="keypad-plus"/></template>
<template v-else>{{ k }}</template>
</button>
</div>
<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">
<span class="visually-hidden" text-translate="true">Loading...</span>
</div>
<template v-else>
<span text-translate="true">Charge</span>
</template>
</button>
<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">
<vc:icon symbol="nav-invoice"/>
</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">
<vc:icon symbol="nav-mobile-menu"/>
</button>
<div class="offcanvas offcanvas-end" data-bs-backdrop="static" tabindex="-1" id="ItemsListOffcanvas" aria-labelledby="ItemsListToggle" v-if="showItems">
<div class="offcanvas-header justify-content-between 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="@StringLocalizer["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>
</div>
}
</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 == AppItemPriceType.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))
{
<div class="img d-none d-sm-block">
<img src="@item.Image" alt="@item.Title" asp-append-version="true" />
</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 == AppItemPriceType.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)">
@if (item.Inventory > 0)
{
<span>@ViewLocalizer["{0} left", item.Inventory.ToString()]</span>
}
else
{
<span text-translate="true">Sold out</span>
}
</span>
}
</div>
</div>
<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>
</button>
<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>
</button>
</div>
</div>
</div>
}
</div>
</div>
</div>
</form>