btcpayserver/BTCPayServer/Views/Shared/PointOfSale/Public/Cart.cshtml
2024-08-20 18:12:32 +02:00

260 lines
17 KiB
Text

@using BTCPayServer.Plugins.PointOfSale.Models
@using BTCPayServer.Services
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using Newtonsoft.Json.Linq
@using BTCPayServer.Client
@using BTCPayServer.Abstractions.TagHelpers
@inject DisplayFormatter DisplayFormatter
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
@{
Layout = "PointOfSale/Public/_Layout";
Csp.UnsafeEval();
}
@section PageHeadContent {
<link href="~/pos/cart.css" asp-append-version="true" rel="stylesheet" />
}
@section PageFootContent {
<script>const srvModel = @Safe.Json(Model);</script>
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
<script src="~/pos/common.js" asp-append-version="true"></script>
<script src="~/pos/cart.js" asp-append-version="true"></script>
}
@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;
}
}
<div id="PosCart">
<div id="content">
<div class="public-page-wrap">
<header class="sticky-top bg-body d-flex flex-column py-3 py-lg-4 gap-3">
<div class="d-flex align-items-center justify-content-center gap-3 px-5 position-relative">
<h1 class="mb-0">@(string.IsNullOrEmpty(Model.Title) ? Model.StoreName : Model.Title)</h1>
<button id="CartToggle" class="cart-toggle-btn" type="button" v-on:click="toggleCart" aria-controls="cart" :disabled="cartCount === 0">
<vc:icon symbol="pos-cart" />
<span id="CartBadge" class="badge rounded-pill bg-danger p-1 ms-1" v-text="cartCount" v-if="cartCount !== 0"></span>
</button>
<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>
</div>
@if (Model.ShowSearch)
{
<input id="SearchTerm" class="form-control rounded-pill" placeholder="Search…" v-model="searchTerm" v-if="showSearch">
}
@if (Model.ShowCategories)
{
<div id="Categories" ref="categories" v-if="showCategories && allCategories" class="btcpay-pills d-flex flex-wrap align-items-center justify-content-center gap-3" :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>
}
</header>
<main>
<partial name="_StatusMessage" />
@if (!string.IsNullOrEmpty(Model.Description))
{
<div class="lead">@Safe.Raw(Model.Description)</div>
}
<div ref="posItems" class="row row-cols row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-2 row-cols-xl-3 row-cols-xxl-4 g-4" 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 buttonText = string.IsNullOrEmpty(item.BuyButtonText)
? item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? Model.CustomButtonText : Model.ButtonText
: item.BuyButtonText;
buttonText = buttonText.Replace("{0}", formatted).Replace("{Price}", formatted);
var categories = new JArray(item.Categories ?? new object[] { });
<div class="col posItem posItem--displayed" :class="{ 'posItem--inStock': inStock(@index) }" data-index="@index" data-search="@Safe.RawEncode(item.Title + " " + item.Description)" data-categories='@Safe.Json(categories)'>
<div class="tile card" v-on:click="addToCart(@index, 1)">
@if (!string.IsNullOrWhiteSpace(item.Image))
{
<img class="card-img-top" src="@item.Image" alt="@item.Title" asp-append-version="true">
}
<div class="card-body d-flex flex-column gap-2 mb-auto">
<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>
}
</div>
@if (!string.IsNullOrWhiteSpace(item.Description))
{
<p class="card-text">@Safe.Raw(item.Description)</p>
}
</div>
@if (inStock)
{
<form class="card-footer">
@if (item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Fixed)
{
<div class="input-group mb-2">
<span class="input-group-text">@Model.CurrencySymbol</span>
<input class="form-control" type="number" min="@(item.Price ?? 0)" step="@Model.Step" name="amount" placeholder="Amount" value="@item.Price" required v-on:click.stop>
</div>
}
<button type="button" class="btn btn-primary w-100" :disabled="!inStock(@index)">
@Safe.Raw(buttonText)
</button>
</form>
<div class="posItem-added"><vc:icon symbol="checkmark" /></div>
}
</div>
</div>
}
</div>
</main>
<footer class="store-footer">
<a class="store-powered-by" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">
Powered by <partial name="_StoreFooterLogo" />
</a>
</footer>
</div>
</div>
<aside id="cart" ref="cart" tabindex="-1" aria-labelledby="cartLabel">
<div class="public-page-wrap" v-cloak>
<header class="sticky-top bg-tile offcanvas-header py-3 py-lg-4 d-flex align-items-baseline justify-content-center gap-3 px-5 pe-lg-0">
<h1 class="mb-0" id="cartLabel">Cart</h1>
<button id="CartClear" type="reset" v-on:click="clear" class="btn btn-text text-primary p-1" v-if="cartCount > 0">
Empty
</button>
<button id="CartClose" type="button" class="cart-toggle-btn" v-on:click="toggleCart" aria-controls="cart" aria-label="Close">
<vc:icon symbol="cross" />
</button>
</header>
<div class="offcanvas-body py-0">
<form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" v-on:submit="handleFormSubmit" v-if="cartCount !== 0">
<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">
<table class="table table-borderless mt-0 mb-4">
<tbody id="CartItems">
<tr v-for="item in cart" :key="item.id">
<td class="align-middle">
<h6 class="fw-semibold mb-1">{{ item.title }}</h6>
<button type="button" v-on:click="removeFromCart(item.id)" class="btn btn-sm btn-link p-0 text-danger fw-semibold">Remove</button>
</td>
<td class="align-middle">
<div class="d-flex align-items-center gap-2 justify-content-end quantity">
<span class="badge text-bg-warning inventory" v-if="item.inventory">
{{ item.inventory > 0 ? `${item.inventory} left` : "Sold out" }}
</span>
<div class="d-flex align-items-center gap-2 quantities">
<button type="button" v-on:click="updateQuantity(item.id, -1)" class="btn btn-minus">
<span><vc:icon symbol="minus" /></span>
</button>
<input class="form-control hide-number-spin w-50px text-center" type="number" placeholder="Qty" min="1" step="1" :max="item.inventory" v-model.number="item.count">
<button type="button" v-on:click="updateQuantity(item.id, +1)" class="btn btn-plus">
<span><vc:icon symbol="plus" /></span>
</button>
</div>
</div>
</td>
<td class="align-middle text-end">
{{ formatCurrency(item.price||0, true) }}
</td>
</tr>
</tbody>
</table>
<table class="table table-borderless my-4" v-if="showDiscount || enableTips">
<tr v-if="showDiscount">
<th class="align-middle">Discount</th>
<th class="align-middle" colspan="3">
<div class="input-group input-group-sm w-100px pull-right">
<input class="form-control hide-number-spin" type="number" min="0" step="1" max="100" id="Discount" v-model.number="discountPercent">
<span class="input-group-text">%</span>
</div>
</th>
</tr>
<tr v-if="enableTips">
<th class="align-middle">Tip</th>
<th class="align-middle" colspan="3">
<div class="btcpay-pills d-flex flex-wrap align-items-center justify-content-end gap-2" v-if="customTipPercentages">
<div class="btcpay-pill d-flex align-items-center px-3" id="Tip-Custom" :class="{ active: !tipPercent && tip }" v-on:click.prevent="tipPercent = null">
<input
v-model.number="tip"
class="form-control hide-number-spin shadow-none text-reset d-block bg-transparent border-0 p-0 me-1 fw-semibold"
style="height:1.5em;min-height:auto;width:4ch"
type="number"
min="0"
step="@Model.Step" />
<span>@(Model.CurrencyInfo.CurrencySymbol ?? Model.CurrencyCode)</span>
</div>
<button
v-for="percentage in customTipPercentages"
type="button"
class="btcpay-pill px-3"
:class="{ active: tipPercent == percentage }"
:id="`Tip-${percentage}`"
v-on:click.prevent="tipPercentage(percentage)">
{{ percentage }}%
</button>
</div>
</th>
</tr>
</table>
<table class="table table-borderless mt-4 mb-0">
<tr>
<td class="align-middle">Subtotal</td>
<td class="align-middle text-end" id="CartAmount">{{ formatCurrency(amountNumeric, true) }}</td>
</tr>
<tr v-if="discountNumeric">
<td class="align-middle">Discount</td>
<td class="align-middle text-end" id="CartDiscount">
<span v-if="discountPercent">{{discountPercent}}% =</span>
{{ formatCurrency(discountNumeric, true) }}
</td>
</tr>
<tr v-if="tipNumeric">
<td class="align-middle">Tip</td>
<td class="align-middle text-end" id="CartTip">
<span v-if="tipPercent">{{tipPercent}}% =</span>
{{ formatCurrency(tipNumeric, true) }}
</td>
</tr>
<tr>
<td class="align-middle h5 border-0">Total</td>
<td class="align-middle h5 border-0 text-end" id="CartTotal">{{ formatCurrency(totalNumeric, true) }}</td>
</tr>
<tr>
<td colspan="2" class="pt-4">
<button id="CartSubmit" class="btn btn-primary btn-lg w-100" :disabled="payButtonLoading" type="submit">
<div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<template v-else>Pay</template>
</button>
</td>
</tr>
</table>
</form>
<p id="CartItems" v-else class="text-muted text-center my-0">There are no items in your cart yet.</p>
</div>
</div>
</aside>
<partial name="PointOfSale/Public/RecentTransactions" model="Model"/>
</div>