diff --git a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs index d174d7475..63cbea66b 100644 --- a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs +++ b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs @@ -618,12 +618,13 @@ namespace BTCPayServer.Tests user.RegisterDerivationScheme("LTC"); var apps = user.GetController(); var vm = Assert.IsType(Assert.IsType(apps.CreateApp(user.StoreId)).Model); + var appType = AppType.PointOfSale.ToString(); vm.AppName = "test"; - vm.SelectedAppType = AppType.PointOfSale.ToString(); + vm.SelectedAppType = appType; Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); var app = appList.Apps[0]; - apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); + apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType }); var vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync(); vmpos.Title = "hello"; vmpos.Currency = "CAD"; diff --git a/BTCPayServer.Tests/CrowdfundTests.cs b/BTCPayServer.Tests/CrowdfundTests.cs index 2a69bdba1..f416100dc 100644 --- a/BTCPayServer.Tests/CrowdfundTests.cs +++ b/BTCPayServer.Tests/CrowdfundTests.cs @@ -35,15 +35,16 @@ namespace BTCPayServer.Tests var apps = user.GetController(); var apps2 = user2.GetController(); var vm = Assert.IsType(Assert.IsType(apps.CreateApp(user.StoreId)).Model); + var appType = AppType.Crowdfund.ToString(); Assert.NotNull(vm.SelectedAppType); Assert.Null(vm.AppName); vm.AppName = "test"; - vm.SelectedAppType = AppType.Crowdfund.ToString(); + vm.SelectedAppType = appType; var redirectToAction = Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); Assert.Equal(nameof(apps.UpdateCrowdfund), redirectToAction.ActionName); var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); var app = appList.Apps[0]; - apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); + apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType}); var appList2 = Assert.IsType(Assert.IsType(apps2.ListApps(user2.StoreId).Result).Model); Assert.Single(appList.Apps); @@ -71,12 +72,13 @@ namespace BTCPayServer.Tests user.RegisterDerivationScheme("BTC"); var apps = user.GetController(); var vm = apps.CreateApp(user.StoreId).AssertViewModel(); + var appType = AppType.Crowdfund.ToString(); vm.AppName = "test"; - vm.SelectedAppType = AppType.Crowdfund.ToString(); + vm.SelectedAppType = appType; Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); var app = appList.Apps[0]; - apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); + apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType }); //Scenario 1: Not Enabled - Not Allowed var crowdfundViewModel = await apps.UpdateCrowdfund(app.Id).AssertViewModelAsync(); @@ -158,12 +160,13 @@ namespace BTCPayServer.Tests await user.SetNetworkFeeMode(NetworkFeeMode.Never); var apps = user.GetController(); var vm = Assert.IsType(Assert.IsType(apps.CreateApp(user.StoreId)).Model); + var appType = AppType.Crowdfund.ToString(); vm.AppName = "test"; - vm.SelectedAppType = AppType.Crowdfund.ToString(); + vm.SelectedAppType = appType; Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); var app = appList.Apps[0]; - apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); + apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType }); TestLogs.LogInformation("We create an invoice with a hardcap"); var crowdfundViewModel = await apps.UpdateCrowdfund(app.Id).AssertViewModelAsync(); diff --git a/BTCPayServer.Tests/POSTests.cs b/BTCPayServer.Tests/POSTests.cs index 454835013..351221580 100644 --- a/BTCPayServer.Tests/POSTests.cs +++ b/BTCPayServer.Tests/POSTests.cs @@ -29,12 +29,13 @@ namespace BTCPayServer.Tests user.RegisterDerivationScheme("BTC"); var apps = user.GetController(); var vm = Assert.IsType(Assert.IsType(apps.CreateApp(user.StoreId)).Model); + var appType = AppType.PointOfSale.ToString(); vm.AppName = "test"; - vm.SelectedAppType = AppType.PointOfSale.ToString(); + vm.SelectedAppType = appType; Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); var app = appList.Apps[0]; - apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); + apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType }); var vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync(); vmpos.Template = @" apple: diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index fa20f7560..37dfbf644 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -1953,17 +1953,18 @@ namespace BTCPayServer.Tests var apps = user.GetController(); var apps2 = user2.GetController(); var vm = Assert.IsType(Assert.IsType(apps.CreateApp(user.StoreId)).Model); + var appType = AppType.PointOfSale.ToString(); Assert.NotNull(vm.SelectedAppType); Assert.Null(vm.AppName); vm.AppName = "test"; - vm.SelectedAppType = AppType.PointOfSale.ToString(); + vm.SelectedAppType = appType; var redirectToAction = Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); Assert.Equal(nameof(apps.UpdatePointOfSale), redirectToAction.ActionName); var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); var appList2 = Assert.IsType(Assert.IsType(apps2.ListApps(user2.StoreId).Result).Model); var app = appList.Apps[0]; - apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); + apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType }); Assert.Single(appList.Apps); Assert.Empty(appList2.Apps); Assert.Equal("test", appList.Apps[0].AppName); diff --git a/BTCPayServer/Components/AppSales/AppSalesViewModel.cs b/BTCPayServer/Components/AppSales/AppSalesViewModel.cs index 75880fe20..19f64fd6c 100644 --- a/BTCPayServer/Components/AppSales/AppSalesViewModel.cs +++ b/BTCPayServer/Components/AppSales/AppSalesViewModel.cs @@ -1,4 +1,3 @@ -using System.Collections; using System.Collections.Generic; using BTCPayServer.Data; using BTCPayServer.Services.Apps; diff --git a/BTCPayServer/Components/AppSales/Default.cshtml b/BTCPayServer/Components/AppSales/Default.cshtml index 5cdc9d321..5c58ddbd9 100644 --- a/BTCPayServer/Components/AppSales/Default.cshtml +++ b/BTCPayServer/Components/AppSales/Default.cshtml @@ -1,19 +1,23 @@ +@using BTCPayServer.Services.Apps +@using BTCPayServer.TagHelpers +@using Microsoft.AspNetCore.Mvc.TagHelpers @model BTCPayServer.Components.AppSales.AppSalesViewModel @{ var action = $"Update{Model.App.AppType}"; + var label = Model.App.AppType == nameof(AppType.Crowdfund) ? "Contributions" : "Sales"; }
-

@Model.App.Name Contributions

+

@Model.App.Name @label

Manage
-

@Model.SalesCount Total Contributions

-
+

@Model.SalesCount Total @label

+
- @foreach (var entry in Model.Entries) + @for (var i = 0; i < Model.Entries.Count; i++) { -
- @entry.Title + var entry = Model.Entries[i]; +
+ + + @entry.Title + - @entry.SalesCount sale@(entry.SalesCount == 1 ? "" : "s"), - @entry.TotalFormatted total + @entry.SalesCount @($"{label}{(entry.SalesCount == 1 ? "" : "s")}"), + @entry.TotalFormatted
} @@ -27,7 +49,7 @@ else {

- No contributions have been made yet. + No @($"{label}s") have been made yet.

}
diff --git a/BTCPayServer/Controllers/UIAppsController.Crowdfund.cs b/BTCPayServer/Controllers/UIAppsController.Crowdfund.cs index 104e895bd..a4678422c 100644 --- a/BTCPayServer/Controllers/UIAppsController.Crowdfund.cs +++ b/BTCPayServer/Controllers/UIAppsController.Crowdfund.cs @@ -62,7 +62,7 @@ namespace BTCPayServer.Controllers IsRecurring = resetEvery != nameof(CrowdfundResetEvery.Never), UseAllStoreInvoices = app.TagAllInvoices, AppId = appId, - SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}", + SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetAppOrderId(app)}", DisplayPerksRanking = settings.DisplayPerksRanking, DisplayPerksValue = settings.DisplayPerksValue, SortPerksByPopularity = settings.SortPerksByPopularity, diff --git a/BTCPayServer/Controllers/UIAppsController.PointOfSale.cs b/BTCPayServer/Controllers/UIAppsController.PointOfSale.cs index 71a4da57f..bf957765e 100644 --- a/BTCPayServer/Controllers/UIAppsController.PointOfSale.cs +++ b/BTCPayServer/Controllers/UIAppsController.PointOfSale.cs @@ -49,7 +49,7 @@ namespace BTCPayServer.Controllers Description = settings.Description, NotificationUrl = settings.NotificationUrl, RedirectUrl = settings.RedirectUrl, - SearchTerm = $"storeid:{app.StoreDataId}", + SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetAppOrderId(app)}", RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : "", RequiresRefundEmail = settings.RequiresRefundEmail }; diff --git a/BTCPayServer/Controllers/UIAppsPublicController.cs b/BTCPayServer/Controllers/UIAppsPublicController.cs index 6fdce9295..66fb5a226 100644 --- a/BTCPayServer/Controllers/UIAppsPublicController.cs +++ b/BTCPayServer/Controllers/UIAppsPublicController.cs @@ -223,7 +223,7 @@ namespace BTCPayServer.Controllers Currency = settings.Currency, Price = price, BuyerEmail = email, - OrderId = orderId ?? AppService.GetPosOrderId(appId), + OrderId = orderId ?? AppService.GetAppOrderId(app), NotificationURL = string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl, RedirectURL = !string.IsNullOrEmpty(redirectUrl) ? redirectUrl @@ -378,7 +378,7 @@ namespace BTCPayServer.Controllers { var invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest() { - OrderId = AppService.GetCrowdfundOrderId(appId), + OrderId = AppService.GetAppOrderId(app), Currency = settings.TargetCurrency, ItemCode = request.ChoiceKey ?? string.Empty, ItemDesc = title, diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index 6d1e22df7..901d966c2 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -237,6 +237,12 @@ namespace BTCPayServer var lnAddress = username is null ? null : $"{username}@{Request.Host}"; List lnurlMetadata = new(); + var redirectUrl = app?.AppType switch + { + nameof(AppType.PointOfSale) => app.GetSettings().RedirectUrl ?? + HttpContext.Request.GetAbsoluteUri($"/apps/{app.Id}/pos"), + _ => null + }; var invoiceRequest = new CreateInvoiceRequest { Amount = invoiceAmount, @@ -245,7 +251,8 @@ namespace BTCPayServer PaymentMethods = new[] { pmi.ToStringNormalized() }, Expiration = blob.InvoiceExpiration < TimeSpan.FromMinutes(2) ? blob.InvoiceExpiration - : TimeSpan.FromMinutes(2) + : TimeSpan.FromMinutes(2), + RedirectURL = redirectUrl }, Currency = currencyCode, Type = invoiceAmount is null ? InvoiceType.TopUp : InvoiceType.Standard, @@ -258,7 +265,7 @@ namespace BTCPayServer { ItemCode = item.Id, ItemDesc = item.Description, - OrderId = AppService.GetPosOrderId(app.Id) + OrderId = AppService.GetAppOrderId(app) }.ToJObject(); } diff --git a/BTCPayServer/Controllers/UIStoresController.cs b/BTCPayServer/Controllers/UIStoresController.cs index 9973db949..9a371174c 100644 --- a/BTCPayServer/Controllers/UIStoresController.cs +++ b/BTCPayServer/Controllers/UIStoresController.cs @@ -158,17 +158,16 @@ namespace BTCPayServer.Controllers var userId = GetUserId(); var apps = await _appService.GetAllApps(userId, false, store.Id); vm.Apps = apps - .Where(a => a.AppType == AppType.Crowdfund.ToString()) .Select(a => { - var appData = _appService.GetAppDataIfOwner(userId, a.Id, AppType.Crowdfund).Result; + var appData = _appService.GetAppDataIfOwner(userId, a.Id).Result; appData.StoreData = store; return appData; }) .ToList(); } - return View("Dashboard", vm); + return View(vm); } [HttpPost] diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index 6a27ea20b..c0be47787 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Client.Models; -using BTCPayServer.Controllers; using BTCPayServer.Data; using BTCPayServer.Models.AppViewModels; using BTCPayServer.Payments; @@ -17,6 +16,7 @@ using Ganss.XSS; using Microsoft.EntityFrameworkCore; using NBitcoin; using NBitcoin.DataEncoders; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using YamlDotNet.Core; using YamlDotNet.RepresentationModel; @@ -46,7 +46,7 @@ namespace BTCPayServer.Services.Apps _storeRepository = storeRepository; _HtmlSanitizer = htmlSanitizer; } - + public async Task GetAppInfo(string appId) { var app = await GetApp(appId, AppType.Crowdfund, true); @@ -200,8 +200,9 @@ namespace BTCPayServer.Services.Apps var currencyData = _Currencies.GetCurrencyData(settings.TargetCurrency, true); var perks = Parse(settings.PerksTemplate, settings.TargetCurrency); var perkCount = paidInvoices - .Where(entity => !string.IsNullOrEmpty(entity.Metadata.ItemCode) && - entity.Currency.Equals(settings.TargetCurrency, StringComparison.OrdinalIgnoreCase)) + .Where(entity => entity.Currency.Equals(settings.TargetCurrency, StringComparison.OrdinalIgnoreCase) && + // we need the item code to know which perk it is and group by that + !string.IsNullOrEmpty(entity.Metadata.ItemCode)) .GroupBy(entity => entity.Metadata.ItemCode) .Select(entities => { @@ -229,14 +230,67 @@ namespace BTCPayServer.Services.Apps return perkCount; } - public async Task GetSalesStats(AppData appData, int numberOfDays = 7) + public async Task> GetItemStats(AppData appData) { + var settings = appData.GetSettings(); var invoices = await GetInvoicesForApp(appData); var paidInvoices = invoices.Where(IsPaid).ToArray(); + var currencyData = _Currencies.GetCurrencyData(settings.Currency, true); + var items = Parse(settings.Template, settings.Currency); + var itemCount = paidInvoices + .Where(entity => entity.Currency.Equals(settings.Currency, StringComparison.OrdinalIgnoreCase) && ( + // The POS data is present for the cart view, where multiple items can be bought + !string.IsNullOrEmpty(entity.Metadata.PosData) || + // The item code should be present for all types other than the cart and keypad + !string.IsNullOrEmpty(entity.Metadata.ItemCode) + )) + .Aggregate(new List(), AggregateInvoiceEntitiesForStats(items)) + .GroupBy(entity => entity.ItemCode) + .Select(entities => + { + var total = entities.Sum(entity => entity.FiatPrice); + var itemCode = entities.Key; + var item = items.FirstOrDefault(p => p.Id == itemCode); + return new ItemStats + { + ItemCode = itemCode, + Title = item?.Title ?? itemCode, + SalesCount = entities.Count(), + Total = total, + TotalFormatted = $"{total.ShowMoney(currencyData.Divisibility)} {settings.Currency}" + }; + }) + .OrderByDescending(stats => stats.SalesCount); + + return itemCount; + } + + public async Task GetSalesStats(AppData app, int numberOfDays = 7) + { + ViewPointOfSaleViewModel.Item[] items = null; + switch (app.AppType) + { + case nameof(AppType.Crowdfund): + var cfS = app.GetSettings(); + items = Parse(cfS.PerksTemplate, cfS.TargetCurrency); + break; + case nameof(AppType.PointOfSale): + var posS = app.GetSettings(); + items = Parse(posS.Template, posS.Currency); + break; + } + + var invoices = await GetInvoicesForApp(app); + var paidInvoices = invoices.Where(IsPaid).ToArray(); var series = paidInvoices - .Where(entity => !string.IsNullOrEmpty(entity.Metadata.ItemCode) && - entity.InvoiceTime > DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays)) - .GroupBy(entity => entity.InvoiceTime.Date) + .Where(entity => entity.InvoiceTime > DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays) && ( + // The POS data is present for the cart view, where multiple items can be bought + !string.IsNullOrEmpty(entity.Metadata.PosData) || + // The item code should be present for all types other than the cart and keypad + !string.IsNullOrEmpty(entity.Metadata.ItemCode) + )) + .Aggregate(new List(), AggregateInvoiceEntitiesForStats(items)) + .GroupBy(entity => entity.Date) .Select(entities => new SalesStatsItem { Date = entities.Key, @@ -260,18 +314,79 @@ namespace BTCPayServer.Services.Apps return new SalesStats { - SalesCount = paidInvoices.Length, + SalesCount = series.Sum(i => i.SalesCount), Series = series.OrderBy(i => i.Label) }; } + private class InvoiceStatsItem + { + public string ItemCode { get; set; } + public decimal FiatPrice { get; set; } + public DateTime Date { get; set; } + } + + private static Func, InvoiceEntity, List> AggregateInvoiceEntitiesForStats(ViewPointOfSaleViewModel.Item[] items) + { + return (res, e) => + { + if (!string.IsNullOrEmpty(e.Metadata.ItemCode)) + { + var item = items.FirstOrDefault(p => p.Id == e.Metadata.ItemCode); + if (item == null) return res; + + var fiatPrice = e.GetPayments(true).Sum(pay => + { + var paymentMethodId = pay.GetPaymentMethodId(); + var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee; + var rate = e.GetPaymentMethod(paymentMethodId).Rate; + return rate * value; + }); + res.Add(new InvoiceStatsItem + { + ItemCode = e.Metadata.ItemCode, + FiatPrice = fiatPrice, + Date = e.InvoiceTime.Date + }); + } + else if (!string.IsNullOrEmpty(e.Metadata.PosData)) + { + // flatten single items from POS data + var data = JsonConvert.DeserializeObject(e.Metadata.PosData); + if (data is not { Cart.Length: > 0 }) return res; + foreach (var lineItem in data.Cart) + { + var item = items.FirstOrDefault(p => p.Id == lineItem.Id); + if (item == null) continue; + + for (var i = 0; i < lineItem.Count; i++) + { + res.Add(new InvoiceStatsItem + { + ItemCode = item.Id, + FiatPrice = lineItem.Price.Value, + Date = e.InvoiceTime.Date + }); + } + } + } + return res; + }; + } + private static bool IsPaid(InvoiceEntity entity) { return entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed || entity.Status == InvoiceStatusLegacy.Paid; } - public static string GetPosOrderId(string appId) => $"pos-app_{appId}"; - public static string GetCrowdfundOrderId(string appId) => $"crowdfund-app_{appId}"; + public static string GetAppOrderId(AppData app) => + app.AppType switch + { + nameof(AppType.Crowdfund) => $"crowdfund-app_{app.Id}", + nameof(AppType.PointOfSale) => $"pos-app_{app.Id}", + _ => throw new ArgumentOutOfRangeException(nameof(app), app.AppType) + }; + public static string GetAppInternalTag(string appId) => $"APP#{appId}"; public static string[] GetAppInternalTags(InvoiceEntity invoice) { @@ -283,7 +398,7 @@ namespace BTCPayServer.Services.Apps var invoices = await _InvoiceRepository.GetInvoices(new InvoiceQuery() { StoreId = new[] { appData.StoreData.Id }, - OrderId = appData.TagAllInvoices ? null : new[] { GetCrowdfundOrderId(appData.Id) }, + OrderId = appData.TagAllInvoices ? null : new[] { GetAppOrderId(appData) }, Status = new[]{ InvoiceState.ToString(InvoiceStatusLegacy.New), InvoiceState.ToString(InvoiceStatusLegacy.Paid), diff --git a/BTCPayServer/Services/Invoices/PosAppData.cs b/BTCPayServer/Services/Invoices/PosAppData.cs new file mode 100644 index 000000000..3eff3a41b --- /dev/null +++ b/BTCPayServer/Services/Invoices/PosAppData.cs @@ -0,0 +1,61 @@ +using BTCPayServer.Models.AppViewModels; +using Newtonsoft.Json; + +namespace BTCPayServer.Services.Invoices; + +public class PosAppData +{ + [JsonProperty(PropertyName = "cart")] + public PosAppCartItem[] Cart { get; set; } + + [JsonProperty(PropertyName = "customAmount")] + public decimal CustomAmount { get; set; } + + [JsonProperty(PropertyName = "discountPercentage")] + public decimal DiscountPercentage { get; set; } + + [JsonProperty(PropertyName = "discountAmount")] + public decimal DiscountAmount { get; set; } + + [JsonProperty(PropertyName = "tip")] + public decimal Tip { get; set; } + + [JsonProperty(PropertyName = "subTotal")] + public decimal Subtotal { get; set; } + + [JsonProperty(PropertyName = "total")] + public decimal Total { get; set; } +} + +public class PosAppCartItem +{ + [JsonProperty(PropertyName = "id")] + public string Id { get; set; } + + [JsonProperty(PropertyName = "price")] + public PosAppCartItemPrice Price { get; set; } + + [JsonProperty(PropertyName = "title")] + public string Title { get; set; } + + [JsonProperty(PropertyName = "count")] + public int Count { get; set; } + + [JsonProperty(PropertyName = "inventory")] + public int? Inventory { get; set; } + + [JsonProperty(PropertyName = "image")] + public string Image { get; set; } +} + +public class PosAppCartItemPrice +{ + [JsonProperty(PropertyName = "formatted")] + public string Formatted { get; set; } + + [JsonProperty(PropertyName = "value")] + public decimal Value { get; set; } + + [JsonProperty(PropertyName = "type")] + public ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType Type { get; set; } +} diff --git a/BTCPayServer/Views/UIApps/UpdatePointOfSale.cshtml b/BTCPayServer/Views/UIApps/UpdatePointOfSale.cshtml index e42c69357..41dd79541 100644 --- a/BTCPayServer/Views/UIApps/UpdatePointOfSale.cshtml +++ b/BTCPayServer/Views/UIApps/UpdatePointOfSale.cshtml @@ -256,7 +256,7 @@ diff --git a/BTCPayServer/wwwroot/main/site.css b/BTCPayServer/wwwroot/main/site.css index 2e8aa34d7..c3e5d53b4 100644 --- a/BTCPayServer/wwwroot/main/site.css +++ b/BTCPayServer/wwwroot/main/site.css @@ -1,3 +1,13 @@ +/* Variables */ +:root { + --chart-main-rgb: 68, 164, 49; + --chart-series-a-rgb: var(--chart-main-rgb); + --chart-series-b-rgb: 245, 0, 0; + --chart-series-c-rgb: 0, 109, 242; + --chart-series-d-rgb: 255, 188, 4; + --chart-series-e-rgb: 160, 98, 75; +} + /* General and site-wide Bootstrap modifications */ p { margin-bottom: 1.5rem; @@ -103,7 +113,6 @@ h2 small .fa-question-circle-o { font-size: var(--btcpay-font-size-l); } - /* Invoices */ .invoice-payments { padding: var(--btcpay-space-m) var(--btcpay-space-l); @@ -391,6 +400,27 @@ svg.icon-note { font-weight: var(--btcpay-font-weight-semibold); } +.widget.app-top-items .ct-chart, +.widget.app-top-items .ct-chart .ct-chart-bar { + height: 30px; +} + +.widget.app-top-items .ct-chart .ct-chart-bar { + margin-left: -.4rem; + margin-right: -.5rem; + width: calc(100% + 1rem) !important; +} + +.widget.app-top-items .ct-bar { + stroke-linecap: round; + stroke-width: 10px; +} + +.widget.app-top-items .ct-grids, +.widget.app-top-items .ct-labels { + display: none; +} + .widget.app-top-items .app-items { display: flex; flex-direction: column; @@ -399,10 +429,20 @@ svg.icon-note { .widget.app-top-items .app-item { display: flex; - align-items: end; + flex-wrap: wrap; + gap: var(--btcpay-space-xs); + align-items: center; justify-content: space-between; } +.widget.app-top-items .app-item-point { + display: inline-block; + width: var(--btcpay-space-s); + height: var(--btcpay-space-s); + margin-right: var(--btcpay-space-s); + border-radius: 50%; +} + .widget.app-top-items .app-item-value { font-weight: var(--btcpay-font-weight-semibold); } diff --git a/BTCPayServer/wwwroot/vendor/chartist/chartist.css b/BTCPayServer/wwwroot/vendor/chartist/chartist.css index fc5601bd1..323e1d231 100644 --- a/BTCPayServer/wwwroot/vendor/chartist/chartist.css +++ b/BTCPayServer/wwwroot/vendor/chartist/chartist.css @@ -1,9 +1,12 @@ .ct-label { - fill: rgba(128, 128, 128, .4); color: var(--btcpay-body-text-muted); font-size: 0.75rem; line-height: 1; } +.ct-chart-pie .ct-label { + fill: rgba(var(--btcpay-body-text-rgb), 1); +} + .ct-label.ct-horizontal { min-width: 3rem; } @@ -168,35 +171,40 @@ fill: none; stroke-width: 60px; } +.ct-series-0 .ct-point { background: rgb(var(--chart-series-a-rgb)); } .ct-series-a .ct-point, .ct-series-a .ct-line, .ct-series-a .ct-bar, .ct-series-a .ct-slice-donut { - stroke: rgba(68,164,49, 1); } + stroke: rgb(var(--chart-series-a-rgb)); } .ct-series-a .ct-slice-pie, .ct-series-a .ct-slice-donut-solid, .ct-series-a .ct-area { - fill: rgba(68,164,49, 0.75); } + fill: rgb(var(--chart-series-a-rgb)); } +.ct-series-1 .ct-point { background: rgb(var(--chart-series-b-rgb)); } .ct-series-b .ct-point, .ct-series-b .ct-line, .ct-series-b .ct-bar, .ct-series-b .ct-slice-donut { - stroke: #f05b4f; } + stroke: rgb(var(--chart-series-b-rgb)); } .ct-series-b .ct-slice-pie, .ct-series-b .ct-slice-donut-solid, .ct-series-b .ct-area { - fill: #f05b4f; } + fill: rgb(var(--chart-series-c-rgb)); } +.ct-series-2 .ct-point { background: rgb(var(--chart-series-c-rgb)); } .ct-series-c .ct-point, .ct-series-c .ct-line, .ct-series-c .ct-bar, .ct-series-c .ct-slice-donut { - stroke: #f4c63d; } + stroke: rgb(var(--chart-series-c-rgb)); } .ct-series-c .ct-slice-pie, .ct-series-c .ct-slice-donut-solid, .ct-series-c .ct-area { - fill: #f4c63d; } + fill: rgb(var(--chart-series-c-rgb)); } +.ct-series-3 .ct-point { background: rgb(var(--chart-series-d-rgb)); } .ct-series-d .ct-point, .ct-series-d .ct-line, .ct-series-d .ct-bar, .ct-series-d .ct-slice-donut { - stroke: #d17905; } + stroke: rgb(var(--chart-series-d-rgb)); } .ct-series-d .ct-slice-pie, .ct-series-d .ct-slice-donut-solid, .ct-series-d .ct-area { - fill: #d17905; } + fill: rgb(var(--chart-series-d-rgb)); } +.ct-series-4 .ct-point { background: rgb(var(--chart-series-e-rgb)); } .ct-series-e .ct-point, .ct-series-e .ct-line, .ct-series-e .ct-bar, .ct-series-e .ct-slice-donut { - stroke: #453d3f; } + stroke: rgb(var(--chart-series-e-rgb)); } .ct-series-e .ct-slice-pie, .ct-series-e .ct-slice-donut-solid, .ct-series-e .ct-area { - fill: #453d3f; } + fill: rgb(var(--chart-series-e-rgb)); } .ct-square { display: block;