mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-11 01:35:22 +01:00
Async dashboard (#3916)
* Dashboard: Load Lightning balance async, display default currency * Simplify approach, improve views and scripts * Async tiles Async tiles * Add period for app sales * Fix missing keypad view sales * Fix after rebase * Fix awaited call * Fix build Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
This commit is contained in:
parent
19aaff2345
commit
657423207b
25 changed files with 459 additions and 265 deletions
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
|
@ -8,26 +9,30 @@ using Microsoft.AspNetCore.Mvc;
|
|||
|
||||
namespace BTCPayServer.Components.AppSales;
|
||||
|
||||
public enum AppSalesPeriod
|
||||
{
|
||||
Week,
|
||||
Month
|
||||
}
|
||||
|
||||
public class AppSales : ViewComponent
|
||||
{
|
||||
private readonly AppService _appService;
|
||||
private readonly StoreRepository _storeRepo;
|
||||
|
||||
public AppSales(AppService appService, StoreRepository storeRepo)
|
||||
public AppSales(AppService appService)
|
||||
{
|
||||
_appService = appService;
|
||||
_storeRepo = storeRepo;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(AppData app)
|
||||
public async Task<IViewComponentResult> InvokeAsync(AppSalesViewModel vm)
|
||||
{
|
||||
var stats = await _appService.GetSalesStats(app);
|
||||
var vm = new AppSalesViewModel
|
||||
{
|
||||
App = app,
|
||||
SalesCount = stats.SalesCount,
|
||||
Series = stats.Series
|
||||
};
|
||||
if (vm.App == null) throw new ArgumentNullException(nameof(vm.App));
|
||||
if (vm.InitialRendering) return View(vm);
|
||||
|
||||
var stats = await _appService.GetSalesStats(vm.App);
|
||||
|
||||
vm.SalesCount = stats.SalesCount;
|
||||
vm.Series = stats.Series;
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ namespace BTCPayServer.Components.AppSales;
|
|||
public class AppSalesViewModel
|
||||
{
|
||||
public AppData App { get; set; }
|
||||
public AppSalesPeriod Period { get; set; } = AppSalesPeriod.Week;
|
||||
public int SalesCount { get; set; }
|
||||
public IEnumerable<SalesStatsItem> Series { get; set; }
|
||||
public bool InitialRendering { get; set; }
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
@using BTCPayServer.Services.Apps
|
||||
@using BTCPayServer.TagHelpers
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Components.AppSales
|
||||
@model BTCPayServer.Components.AppSales.AppSalesViewModel
|
||||
|
||||
@{
|
||||
|
@ -13,22 +14,81 @@
|
|||
<h3>@Model.App.Name @label</h3>
|
||||
<a asp-controller="UIApps" asp-action="@action" asp-route-appId="@Model.App.Id">Manage</a>
|
||||
</header>
|
||||
<p>@Model.SalesCount Total @label</p>
|
||||
<div class="ct-chart"></div>
|
||||
<script>
|
||||
(function () {
|
||||
const id = @Safe.Json($"AppSales-{Model.App.Id}");
|
||||
const labels = @Safe.Json(Model.Series.Select(i => i.Label));
|
||||
const series = @Safe.Json(Model.Series.Select(i => i.SalesCount));
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
|
||||
new Chartist.Bar(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
series: [series]
|
||||
}, {
|
||||
low,
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
<div class="loading d-flex justify-content-center p-3">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Url.Action("AppSales", "UIApps", new { appId = Model.App.Id }));
|
||||
const appId = @Safe.Json(Model.App.Id);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`AppSales-${appId}`).outerHTML = await response.text();
|
||||
const initScript = document.querySelector(`#AppSales-${appId} script`);
|
||||
if (initScript) eval(initScript.innerHTML);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
else
|
||||
{
|
||||
<header class="mb-3">
|
||||
<span>
|
||||
<span class="sales-count">@Model.SalesCount</span> Total @label
|
||||
</span>
|
||||
<div class="btn-group only-for-js" role="group" aria-label="Filter">
|
||||
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.App.Id" id="AppSalesPeriodWeek-@Model.App.Id" value="@AppSalesPeriod.Week" @(Model.Period == AppSalesPeriod.Week ? "checked" : "")>
|
||||
<label class="btn btn-link" for="AppSalesPeriodWeek-@Model.App.Id">1W</label>
|
||||
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.App.Id" id="AppSalesPeriodMonth-@Model.App.Id" value="@AppSalesPeriod.Month" @(Model.Period == AppSalesPeriod.Month ? "checked" : "")>
|
||||
<label class="btn btn-link" for="AppSalesPeriodMonth-@Model.App.Id">1M</label>
|
||||
</div>
|
||||
</header>
|
||||
<div class="ct-chart"></div>
|
||||
<script>
|
||||
(function () {
|
||||
const id = @Safe.Json($"AppSales-{Model.App.Id}");
|
||||
const appId = @Safe.Json(Model.App.Id);
|
||||
const period = @Safe.Json(Model.Period.ToString());
|
||||
const baseUrl = @Safe.Json(Url.Action("AppSales", "UIApps", new { appId = Model.App.Id }));
|
||||
const data = { series: @Safe.Json(Model.Series), salesCount: @Safe.Json(Model.SalesCount) };
|
||||
|
||||
const render = (data, period) => {
|
||||
const series = data.series.map(s => s.salesCount);
|
||||
const labels = data.series.map((s, i) => period === @Safe.Json(Model.Period.ToString()) ? s.label : (i % 5 === 0 ? s.label : ''));
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
|
||||
|
||||
document.querySelectorAll(`#${id} .sales-count`).innerText = data.salesCount;
|
||||
|
||||
new Chartist.Bar(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
series: [series]
|
||||
}, {
|
||||
low,
|
||||
});
|
||||
};
|
||||
|
||||
render(data, period);
|
||||
|
||||
const update = async period => {
|
||||
const url = `${baseUrl}/${period}`;
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
render(data, period);
|
||||
}
|
||||
};
|
||||
|
||||
delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => {
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -12,24 +12,22 @@ namespace BTCPayServer.Components.AppTopItems;
|
|||
public class AppTopItems : ViewComponent
|
||||
{
|
||||
private readonly AppService _appService;
|
||||
private readonly StoreRepository _storeRepo;
|
||||
|
||||
public AppTopItems(AppService appService, StoreRepository storeRepo)
|
||||
public AppTopItems(AppService appService)
|
||||
{
|
||||
_appService = appService;
|
||||
_storeRepo = storeRepo;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(AppData app)
|
||||
public async Task<IViewComponentResult> InvokeAsync(AppTopItemsViewModel vm)
|
||||
{
|
||||
var entries = Enum.Parse<AppType>(app.AppType) == AppType.Crowdfund
|
||||
? await _appService.GetPerkStats(app)
|
||||
: await _appService.GetItemStats(app);
|
||||
var vm = new AppTopItemsViewModel
|
||||
{
|
||||
App = app,
|
||||
Entries = entries.ToList()
|
||||
};
|
||||
if (vm.App == null) throw new ArgumentNullException(nameof(vm.App));
|
||||
if (vm.InitialRendering) return View(vm);
|
||||
|
||||
var entries = Enum.Parse<AppType>(vm.App.AppType) == AppType.Crowdfund
|
||||
? await _appService.GetPerkStats(vm.App)
|
||||
: await _appService.GetItemStats(vm.App);
|
||||
|
||||
vm.Entries = entries.ToList();
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
|
|
@ -8,4 +8,5 @@ public class AppTopItemsViewModel
|
|||
{
|
||||
public AppData App { get; set; }
|
||||
public List<ItemStats> Entries { get; set; }
|
||||
public bool InitialRendering { get; set; }
|
||||
}
|
||||
|
|
|
@ -11,7 +11,27 @@
|
|||
<h3>Top @(Model.App.AppType == nameof(AppType.Crowdfund) ? "Perks" : "Items")</h3>
|
||||
<a asp-controller="UIApps" asp-action="@action" asp-route-appId="@Model.App.Id">View All</a>
|
||||
</header>
|
||||
@if (Model.Entries.Any())
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
<div class="loading d-flex justify-content-center p-3">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Url.Action("AppTopItems", "UIApps", new { appId = Model.App.Id }));
|
||||
const appId = @Safe.Json(Model.App.Id);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`AppTopItems-${appId}`).outerHTML = await response.text();
|
||||
const initScript = document.querySelector(`#AppTopItems-${appId} script`);
|
||||
if (initScript) eval(initScript.innerHTML);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
else if (Model.Entries.Any())
|
||||
{
|
||||
<div class="ct-chart mb-3"></div>
|
||||
<script>
|
||||
|
|
|
@ -179,4 +179,3 @@
|
|||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ namespace BTCPayServer.Components.StoreLightningBalance;
|
|||
|
||||
public class StoreLightningBalance : ViewComponent
|
||||
{
|
||||
private string _cryptoCode;
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly BTCPayServerOptions _btcpayServerOptions;
|
||||
|
@ -44,14 +43,13 @@ public class StoreLightningBalance : ViewComponent
|
|||
_externalServiceOptions = externalServiceOptions;
|
||||
_lightningClientFactory = lightningClientFactory;
|
||||
_lightningNetworkOptions = lightningNetworkOptions;
|
||||
_cryptoCode = _networkProvider.DefaultNetwork.CryptoCode;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreLightningBalanceViewModel vm)
|
||||
{
|
||||
if (vm.Store == null) throw new ArgumentNullException(nameof(vm.Store));
|
||||
if (vm.CryptoCode == null) throw new ArgumentNullException(nameof(vm.CryptoCode));
|
||||
|
||||
vm.CryptoCode = _cryptoCode;
|
||||
vm.DefaultCurrency = vm.Store.GetStoreBlob().DefaultCurrency;
|
||||
vm.CurrencyData = _currencies.GetCurrencyData(vm.DefaultCurrency, true);
|
||||
|
||||
|
@ -59,7 +57,7 @@ public class StoreLightningBalance : ViewComponent
|
|||
|
||||
try
|
||||
{
|
||||
var lightningClient = GetLightningClient(vm.Store);
|
||||
var lightningClient = GetLightningClient(vm.Store, vm.CryptoCode);
|
||||
var balance = await lightningClient.GetBalance();
|
||||
vm.Balance = balance;
|
||||
vm.TotalOnchain = balance.OnchainBalance != null
|
||||
|
@ -84,10 +82,10 @@ public class StoreLightningBalance : ViewComponent
|
|||
return View(vm);
|
||||
}
|
||||
|
||||
private ILightningClient GetLightningClient(StoreData store)
|
||||
private ILightningClient GetLightningClient(StoreData store, string cryptoCode)
|
||||
{
|
||||
var network = _networkProvider.GetNetwork<BTCPayNetwork>(_cryptoCode);
|
||||
var id = new PaymentMethodId(_cryptoCode, PaymentTypes.LightningLike);
|
||||
var network = _networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_networkProvider)
|
||||
.OfType<LightningSupportedPaymentMethod>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
|
@ -97,7 +95,7 @@ public class StoreLightningBalance : ViewComponent
|
|||
{
|
||||
return _lightningClientFactory.Create(connectionString, network);
|
||||
}
|
||||
if (existing.IsInternalNode && _lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(_cryptoCode, out var internalLightningNode))
|
||||
if (existing.IsInternalNode && _lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var internalLightningNode))
|
||||
{
|
||||
return _lightningClientFactory.Create(internalLightningNode, network);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Components.StoreLightningBalance;
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ namespace BTCPayServer.Components.StoreLightningServices;
|
|||
|
||||
public class StoreLightningServices : ViewComponent
|
||||
{
|
||||
private readonly string _cryptoCode;
|
||||
private readonly BTCPayServerOptions _btcpayServerOptions;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
|
||||
|
@ -31,61 +30,55 @@ public class StoreLightningServices : ViewComponent
|
|||
_networkProvider = networkProvider;
|
||||
_btcpayServerOptions = btcpayServerOptions;
|
||||
_externalServiceOptions = externalServiceOptions;
|
||||
_cryptoCode = _networkProvider.DefaultNetwork.CryptoCode;
|
||||
}
|
||||
|
||||
public IViewComponentResult Invoke(StoreData store)
|
||||
public IViewComponentResult Invoke(StoreLightningServicesViewModel vm)
|
||||
{
|
||||
var vm = new StoreLightningServicesViewModel
|
||||
{
|
||||
Store = store,
|
||||
CryptoCode = _cryptoCode,
|
||||
};
|
||||
if (vm.Store == null) throw new ArgumentNullException(nameof(vm.Store));
|
||||
if (vm.CryptoCode == null) throw new ArgumentNullException(nameof(vm.CryptoCode));
|
||||
if (vm.LightningNodeType != LightningNodeType.Internal) return View(vm);
|
||||
|
||||
if (vm.LightningNodeType == LightningNodeType.Internal)
|
||||
{
|
||||
var services = _externalServiceOptions.Value.ExternalServices.ToList()
|
||||
.Where(service => ExternalServices.LightningServiceTypes.Contains(service.Type))
|
||||
.Select(async service =>
|
||||
{
|
||||
var model = new AdditionalServiceViewModel
|
||||
{
|
||||
DisplayName = service.DisplayName,
|
||||
ServiceName = service.ServiceName,
|
||||
CryptoCode = service.CryptoCode,
|
||||
Type = service.Type.ToString()
|
||||
};
|
||||
try
|
||||
{
|
||||
model.Link = await service.GetLink(Request.GetAbsoluteUriNoPathBase(), _btcpayServerOptions.NetworkType);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
model.Error = exception.Message;
|
||||
}
|
||||
return model;
|
||||
})
|
||||
.Select(t => t.Result)
|
||||
.ToList();
|
||||
|
||||
// other services
|
||||
foreach ((string key, Uri value) in _externalServiceOptions.Value.OtherExternalServices)
|
||||
var services = _externalServiceOptions.Value.ExternalServices.ToList()
|
||||
.Where(service => ExternalServices.LightningServiceTypes.Contains(service.Type))
|
||||
.Select(async service =>
|
||||
{
|
||||
if (ExternalServices.LightningServiceNames.Contains(key))
|
||||
var model = new AdditionalServiceViewModel
|
||||
{
|
||||
services.Add(new AdditionalServiceViewModel
|
||||
{
|
||||
DisplayName = key,
|
||||
ServiceName = key,
|
||||
Type = key.Replace(" ", ""),
|
||||
Link = Request.GetAbsoluteUriNoPathBase(value).AbsoluteUri
|
||||
});
|
||||
DisplayName = service.DisplayName,
|
||||
ServiceName = service.ServiceName,
|
||||
CryptoCode = service.CryptoCode,
|
||||
Type = service.Type.ToString()
|
||||
};
|
||||
try
|
||||
{
|
||||
model.Link = await service.GetLink(Request.GetAbsoluteUriNoPathBase(), _btcpayServerOptions.NetworkType);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
model.Error = exception.Message;
|
||||
}
|
||||
return model;
|
||||
})
|
||||
.Select(t => t.Result)
|
||||
.ToList();
|
||||
|
||||
// other services
|
||||
foreach ((string key, Uri value) in _externalServiceOptions.Value.OtherExternalServices)
|
||||
{
|
||||
if (ExternalServices.LightningServiceNames.Contains(key))
|
||||
{
|
||||
services.Add(new AdditionalServiceViewModel
|
||||
{
|
||||
DisplayName = key,
|
||||
ServiceName = key,
|
||||
Type = key.Replace(" ", ""),
|
||||
Link = Request.GetAbsoluteUriNoPathBase(value).AbsoluteUri
|
||||
});
|
||||
}
|
||||
|
||||
vm.Services = services;
|
||||
}
|
||||
|
||||
vm.Services = services;
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,51 @@
|
|||
@model BTCPayServer.Components.StoreNumbers.StoreNumbersViewModel
|
||||
|
||||
<div class="widget store-numbers">
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>Payouts Pending</h6>
|
||||
<a asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.Store.Id">Manage</a>
|
||||
</header>
|
||||
<div class="h3">@Model.PayoutsPending</div>
|
||||
</div>
|
||||
@if (Model.Transactions is not null)
|
||||
{
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>TXs in the last @Model.TransactionDays days</h6>
|
||||
@if (Model.Transactions.Value > 0)
|
||||
{
|
||||
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId">View All</a>
|
||||
}
|
||||
</header>
|
||||
<div class="h3">@Model.Transactions.Value</div>
|
||||
</div>
|
||||
}
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>Refunds Issued</h6>
|
||||
</header>
|
||||
<div class="h3">@Model.RefundsIssued</div>
|
||||
</div>
|
||||
<div class="widget store-numbers" id="StoreNumbers-@Model.Store.Id">
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
<div class="loading d-flex justify-content-center p-3">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Url.Action("StoreNumbers", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
|
||||
const storeId = @Safe.Json(Model.Store.Id);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`StoreNumbers-${storeId}`).outerHTML = await response.text();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>Payouts Pending</h6>
|
||||
<a asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.Store.Id">Manage</a>
|
||||
</header>
|
||||
<div class="h3">@Model.PayoutsPending</div>
|
||||
</div>
|
||||
@if (Model.Transactions is not null)
|
||||
{
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>TXs in the last @Model.TransactionDays days</h6>
|
||||
@if (Model.Transactions.Value > 0)
|
||||
{
|
||||
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId">View All</a>
|
||||
}
|
||||
</header>
|
||||
<div class="h3">@Model.Transactions.Value</div>
|
||||
</div>
|
||||
}
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>Refunds Issued</h6>
|
||||
</header>
|
||||
<div class="h3">@Model.RefundsIssued</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -19,9 +19,6 @@ namespace BTCPayServer.Components.StoreNumbers;
|
|||
|
||||
public class StoreNumbers : ViewComponent
|
||||
{
|
||||
private string CryptoCode;
|
||||
private const int TransactionDays = 7;
|
||||
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
|
@ -40,41 +37,39 @@ public class StoreNumbers : ViewComponent
|
|||
_nbxConnectionFactory = nbxConnectionFactory;
|
||||
_networkProvider = networkProvider;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
CryptoCode = networkProvider.DefaultNetwork.CryptoCode;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreNumbersViewModel vm)
|
||||
{
|
||||
if (vm.Store == null) throw new ArgumentNullException(nameof(vm.Store));
|
||||
if (vm.CryptoCode == null) throw new ArgumentNullException(nameof(vm.CryptoCode));
|
||||
|
||||
vm.WalletId = new WalletId(vm.Store.Id, vm.CryptoCode);
|
||||
|
||||
if (vm.InitialRendering) return View(vm);
|
||||
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var payoutsCount = await ctx.Payouts
|
||||
.Where(p => p.PullPaymentData.StoreId == store.Id && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingApproval)
|
||||
.Where(p => p.PullPaymentData.StoreId == vm.Store.Id && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingApproval)
|
||||
.CountAsync();
|
||||
var refundsCount = await ctx.Invoices
|
||||
.Where(i => i.StoreData.Id == store.Id && !i.Archived && i.CurrentRefundId != null)
|
||||
.Where(i => i.StoreData.Id == vm.Store.Id && !i.Archived && i.CurrentRefundId != null)
|
||||
.CountAsync();
|
||||
|
||||
var walletId = new WalletId(store.Id, CryptoCode);
|
||||
var derivation = store.GetDerivationSchemeSettings(_networkProvider, walletId.CryptoCode);
|
||||
var derivation = vm.Store.GetDerivationSchemeSettings(_networkProvider, vm.CryptoCode);
|
||||
int? transactionsCount = null;
|
||||
if (derivation != null && _nbxConnectionFactory.Available)
|
||||
{
|
||||
await using var conn = await _nbxConnectionFactory.OpenConnection();
|
||||
var wid = NBXplorer.Client.DBUtils.nbxv1_get_wallet_id(derivation.Network.CryptoCode, derivation.AccountDerivation.ToString());
|
||||
var afterDate = DateTimeOffset.UtcNow - TimeSpan.FromDays(TransactionDays);
|
||||
var afterDate = DateTimeOffset.UtcNow - TimeSpan.FromDays(vm.TransactionDays);
|
||||
var count = await conn.ExecuteScalarAsync<long>("SELECT COUNT(*) FROM wallets_history WHERE code=@code AND wallet_id=@wid AND seen_at > @afterDate", new { code = derivation.Network.CryptoCode, wid, afterDate });
|
||||
transactionsCount = (int)count;
|
||||
}
|
||||
|
||||
var vm = new StoreNumbersViewModel
|
||||
{
|
||||
Store = store,
|
||||
WalletId = walletId,
|
||||
PayoutsPending = payoutsCount,
|
||||
Transactions = transactionsCount,
|
||||
TransactionDays = TransactionDays,
|
||||
RefundsIssued = refundsCount
|
||||
};
|
||||
|
||||
vm.PayoutsPending = payoutsCount;
|
||||
vm.Transactions = transactionsCount;
|
||||
vm.RefundsIssued = refundsCount;
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
using System.Collections;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Components.StoreNumbers;
|
||||
|
@ -10,5 +9,7 @@ public class StoreNumbersViewModel
|
|||
public int PayoutsPending { get; set; }
|
||||
public int? Transactions { get; set; }
|
||||
public int RefundsIssued { get; set; }
|
||||
public int TransactionDays { get; set; }
|
||||
public int TransactionDays { get; set; } = 7;
|
||||
public bool InitialRendering { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
@using BTCPayServer.Services.Invoices
|
||||
@model BTCPayServer.Components.StoreRecentInvoices.StoreRecentInvoicesViewModel
|
||||
|
||||
<div class="widget store-recent-transactions">
|
||||
<div class="widget store-recent-invoices" id="StoreRecentInvoices-@Model.Store.Id">
|
||||
<header>
|
||||
<h3>Recent Invoices</h3>
|
||||
@if (Model.Invoices.Any())
|
||||
|
@ -11,7 +11,25 @@
|
|||
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id">View All</a>
|
||||
}
|
||||
</header>
|
||||
@if (Model.Invoices.Any())
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
<div class="loading d-flex justify-content-center p-3">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Url.Action("RecentInvoices", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
|
||||
const storeId = @Safe.Json(Model.Store.Id);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`StoreRecentInvoices-${storeId}`).outerHTML = await response.text();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
else if (Model.Invoices.Any())
|
||||
{
|
||||
<table class="table table-hover mt-3 mb-0">
|
||||
<thead>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -32,22 +33,25 @@ public class StoreRecentInvoices : ViewComponent
|
|||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreRecentInvoicesViewModel vm)
|
||||
{
|
||||
if (vm.Store == null) throw new ArgumentNullException(nameof(vm.Store));
|
||||
if (vm.CryptoCode == null) throw new ArgumentNullException(nameof(vm.CryptoCode));
|
||||
if (vm.InitialRendering) return View(vm);
|
||||
|
||||
var userId = _userManager.GetUserId(UserClaimsPrincipal);
|
||||
var invoiceEntities = await _invoiceRepo.GetInvoices(new InvoiceQuery
|
||||
{
|
||||
UserId = userId,
|
||||
StoreId = new [] { store.Id },
|
||||
StoreId = new [] { vm.Store.Id },
|
||||
IncludeArchived = false,
|
||||
IncludeRefunds = true,
|
||||
Take = 5
|
||||
});
|
||||
var invoices = new List<StoreRecentInvoiceViewModel>();
|
||||
foreach (var invoice in invoiceEntities)
|
||||
{
|
||||
var state = invoice.GetInvoiceState();
|
||||
invoices.Add(new StoreRecentInvoiceViewModel
|
||||
|
||||
vm.Invoices = (from invoice in invoiceEntities
|
||||
let state = invoice.GetInvoiceState()
|
||||
select new StoreRecentInvoiceViewModel
|
||||
{
|
||||
Date = invoice.InvoiceTime,
|
||||
Status = state,
|
||||
|
@ -55,13 +59,7 @@ public class StoreRecentInvoices : ViewComponent
|
|||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.Metadata.OrderId ?? string.Empty,
|
||||
AmountCurrency = _currencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
|
||||
});
|
||||
}
|
||||
var vm = new StoreRecentInvoicesViewModel
|
||||
{
|
||||
Store = store,
|
||||
Invoices = invoices
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
|
|
@ -6,5 +6,7 @@ namespace BTCPayServer.Components.StoreRecentInvoices;
|
|||
public class StoreRecentInvoicesViewModel
|
||||
{
|
||||
public StoreData Store { get; set; }
|
||||
public IEnumerable<StoreRecentInvoiceViewModel> Invoices { get; set; }
|
||||
public IList<StoreRecentInvoiceViewModel> Invoices { get; set; } = new List<StoreRecentInvoiceViewModel>();
|
||||
public bool InitialRendering { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@using BTCPayServer.Abstractions.Extensions
|
||||
@model BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel
|
||||
|
||||
<div class="widget store-recent-transactions">
|
||||
<div class="widget store-recent-transactions" id="StoreRecentTransactions-@Model.Store.Id">
|
||||
<header>
|
||||
<h3>Recent Transactions</h3>
|
||||
@if (Model.Transactions.Any())
|
||||
|
@ -9,7 +9,25 @@
|
|||
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId">View All</a>
|
||||
}
|
||||
</header>
|
||||
@if (Model.Transactions.Any())
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
<div class="loading d-flex justify-content-center p-3">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Url.Action("RecentTransactions", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
|
||||
const storeId = @Safe.Json(Model.Store.Id);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`StoreRecentTransactions-${storeId}`).outerHTML = await response.text();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
else if (Model.Transactions.Any())
|
||||
{
|
||||
<table class="table table-hover mt-3 mb-0">
|
||||
<thead>
|
||||
|
|
|
@ -21,32 +21,27 @@ namespace BTCPayServer.Components.StoreRecentTransactions;
|
|||
|
||||
public class StoreRecentTransactions : ViewComponent
|
||||
{
|
||||
private string CryptoCode;
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; }
|
||||
|
||||
public StoreRecentTransactions(
|
||||
StoreRepository storeRepo,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
NBXplorerConnectionFactory connectionFactory,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
ApplicationDbContextFactory dbContextFactory)
|
||||
BTCPayWalletProvider walletProvider)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
NetworkProvider = networkProvider;
|
||||
_walletProvider = walletProvider;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
CryptoCode = networkProvider.DefaultNetwork.CryptoCode;
|
||||
}
|
||||
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreRecentTransactionsViewModel vm)
|
||||
{
|
||||
var walletId = new WalletId(store.Id, CryptoCode);
|
||||
var derivationSettings = store.GetDerivationSchemeSettings(NetworkProvider, walletId.CryptoCode);
|
||||
if (vm.Store == null) throw new ArgumentNullException(nameof(vm.Store));
|
||||
if (vm.CryptoCode == null) throw new ArgumentNullException(nameof(vm.CryptoCode));
|
||||
|
||||
vm.WalletId = new WalletId(vm.Store.Id, vm.CryptoCode);
|
||||
|
||||
if (vm.InitialRendering) return View(vm);
|
||||
|
||||
var derivationSettings = vm.Store.GetDerivationSchemeSettings(NetworkProvider, vm.CryptoCode);
|
||||
var transactions = new List<StoreRecentTransactionViewModel>();
|
||||
if (derivationSettings?.AccountDerivation is not null)
|
||||
{
|
||||
|
@ -66,13 +61,8 @@ public class StoreRecentTransactions : ViewComponent
|
|||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
var vm = new StoreRecentTransactionsViewModel
|
||||
{
|
||||
Store = store,
|
||||
WalletId = walletId,
|
||||
Transactions = transactions
|
||||
};
|
||||
vm.Transactions = transactions;
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
|
@ -9,4 +8,6 @@ public class StoreRecentTransactionsViewModel
|
|||
public StoreData Store { get; set; }
|
||||
public IList<StoreRecentTransactionViewModel> Transactions { get; set; } = new List<StoreRecentTransactionViewModel>();
|
||||
public WalletId WalletId { get; set; }
|
||||
public bool InitialRendering { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
}
|
||||
|
|
|
@ -24,13 +24,13 @@
|
|||
}
|
||||
@if (Model.Series != null)
|
||||
{
|
||||
<div class="btn-group mt-1" role="group" aria-label="Filter">
|
||||
<input type="radio" class="btn-check" name="filter" id="filter-week" value="week" @(Model.Type == WalletHistogramType.Week ? "checked" : "")>
|
||||
<label class="btn btn-link" for="filter-week">1W</label>
|
||||
<input type="radio" class="btn-check" name="filter" id="filter-month" value="month" @(Model.Type == WalletHistogramType.Month ? "checked" : "")>
|
||||
<label class="btn btn-link" for="filter-month">1M</label>
|
||||
<input type="radio" class="btn-check" name="filter" id="filter-year" value="year" @(Model.Type == WalletHistogramType.Year ? "checked" : "")>
|
||||
<label class="btn btn-link" for="filter-year">1Y</label>
|
||||
<div class="btn-group only-for-js mt-1" role="group" aria-label="Period">
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.Store.Id" id="StoreWalletBalancePeriodWeek-@Model.Store.Id" value="@WalletHistogramType.Week" @(Model.Type == WalletHistogramType.Week ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreWalletBalancePeriodWeek-@Model.Store.Id">1W</label>
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.Store.Id" id="StoreWalletBalancePeriodMonth-@Model.Store.Id" value="@WalletHistogramType.Month" @(Model.Type == WalletHistogramType.Month ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreWalletBalancePeriodMonth-@Model.Store.Id">1M</label>
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.Store.Id" id="StoreWalletBalancePeriodYear-@Model.Store.Id" value="@WalletHistogramType.Year" @(Model.Type == WalletHistogramType.Year ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreWalletBalancePeriodYear-@Model.Store.Id">1Y</label>
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
|
@ -114,7 +114,7 @@
|
|||
render(data);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
delegate('change', `#${id} [name="filter"]`, async e => {
|
||||
delegate('change', `#${id} [name="StoreWalletBalancePeriod-${storeId}"]`, async e => {
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
})
|
||||
|
|
59
BTCPayServer/Controllers/UIAppsController.Dashboard.cs
Normal file
59
BTCPayServer/Controllers/UIAppsController.Dashboard.cs
Normal file
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Components.AppSales;
|
||||
using BTCPayServer.Components.AppTopItems;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIAppsController
|
||||
{
|
||||
[HttpGet("{appId}/dashboard/app-top-items")]
|
||||
public IActionResult AppTopItems(string appId)
|
||||
{
|
||||
var app = HttpContext.GetAppData();
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
app.StoreData = GetCurrentStore();
|
||||
|
||||
var vm = new AppTopItemsViewModel { App = app };
|
||||
return ViewComponent("AppTopItems", new { vm });
|
||||
}
|
||||
|
||||
[HttpGet("{appId}/dashboard/app-sales")]
|
||||
public IActionResult AppSales(string appId)
|
||||
{
|
||||
var app = HttpContext.GetAppData();
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
app.StoreData = GetCurrentStore();
|
||||
|
||||
var vm = new AppSalesViewModel { App = app };
|
||||
return ViewComponent("AppSales", new { vm });
|
||||
}
|
||||
|
||||
[HttpGet("{appId}/dashboard/app-sales/{period}")]
|
||||
public async Task<IActionResult> AppSales(string appId, AppSalesPeriod period)
|
||||
{
|
||||
var app = HttpContext.GetAppData();
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
app.StoreData = GetCurrentStore();
|
||||
|
||||
var days = period switch
|
||||
{
|
||||
AppSalesPeriod.Week => 7,
|
||||
AppSalesPeriod.Month => 30,
|
||||
_ => throw new ArgumentException($"AppSalesPeriod {period} does not exist.")
|
||||
};
|
||||
var stats = await _appService.GetSalesStats(app, days);
|
||||
|
||||
return stats == null
|
||||
? NotFound()
|
||||
: Json(stats);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +1,13 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Components.StoreLightningBalance;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Components.StoreNumbers;
|
||||
using BTCPayServer.Components.StoreRecentInvoices;
|
||||
using BTCPayServer.Components.StoreRecentTransactions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
|
@ -39,36 +30,66 @@ namespace BTCPayServer.Controllers
|
|||
LightningEnabled = lightningEnabled,
|
||||
StoreId = CurrentStore.Id,
|
||||
StoreName = CurrentStore.StoreName,
|
||||
CryptoCode = _NetworkProvider.DefaultNetwork.CryptoCode,
|
||||
IsSetUp = walletEnabled || lightningEnabled
|
||||
};
|
||||
|
||||
// Widget data
|
||||
if (vm.WalletEnabled || vm.LightningEnabled)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
var apps = await _appService.GetAllApps(userId, false, store.Id);
|
||||
vm.Apps = apps
|
||||
.Select(a =>
|
||||
{
|
||||
var appData = _appService.GetAppDataIfOwner(userId, a.Id).Result;
|
||||
appData.StoreData = store;
|
||||
return appData;
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
if (!vm.WalletEnabled && !vm.LightningEnabled) return View(vm);
|
||||
|
||||
var userId = GetUserId();
|
||||
var apps = await _appService.GetAllApps(userId, false, store.Id);
|
||||
foreach (var app in apps)
|
||||
{
|
||||
var appData = await _appService.GetAppDataIfOwner(userId, app.Id);
|
||||
vm.Apps.Add(appData);
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/lightning/{cryptoCode}/balance")]
|
||||
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/lightning/balance")]
|
||||
public IActionResult LightningBalance(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new StoreLightningBalanceViewModel { Store = store };
|
||||
var vm = new StoreLightningBalanceViewModel { Store = store, CryptoCode = cryptoCode };
|
||||
return ViewComponent("StoreLightningBalance", new { vm });
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/numbers")]
|
||||
public IActionResult StoreNumbers(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new StoreNumbersViewModel { Store = store, CryptoCode = cryptoCode };
|
||||
return ViewComponent("StoreNumbers", new { vm });
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/recent-transactions")]
|
||||
public IActionResult RecentTransactions(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new StoreRecentTransactionsViewModel { Store = store, CryptoCode = cryptoCode };
|
||||
return ViewComponent("StoreRecentTransactions", new { vm });
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/recent-invoices")]
|
||||
public IActionResult RecentInvoices(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new StoreRecentInvoicesViewModel { Store = store, CryptoCode = cryptoCode };
|
||||
return ViewComponent("StoreRecentInvoices", new { vm });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,10 @@ namespace BTCPayServer.Models.StoreViewModels;
|
|||
public class StoreDashboardViewModel
|
||||
{
|
||||
public string StoreId { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string StoreName { get; set; }
|
||||
public bool WalletEnabled { get; set; }
|
||||
public bool LightningEnabled { get; set; }
|
||||
public bool IsSetUp { get; set; }
|
||||
public List<AppData> Apps { get; set; }
|
||||
public List<AppData> Apps { get; set; } = new();
|
||||
}
|
||||
|
|
|
@ -283,12 +283,7 @@ namespace BTCPayServer.Services.Apps
|
|||
var invoices = await GetInvoicesForApp(app);
|
||||
var paidInvoices = invoices.Where(IsPaid).ToArray();
|
||||
var series = paidInvoices
|
||||
.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)
|
||||
))
|
||||
.Where(entity => entity.InvoiceTime > DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays))
|
||||
.Aggregate(new List<InvoiceStatsItem>(), AggregateInvoiceEntitiesForStats(items))
|
||||
.GroupBy(entity => entity.Date)
|
||||
.Select(entities => new SalesStatsItem
|
||||
|
@ -330,26 +325,7 @@ namespace BTCPayServer.Services.Apps
|
|||
{
|
||||
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))
|
||||
if (!string.IsNullOrEmpty(e.Metadata.PosData))
|
||||
{
|
||||
// flatten single items from POS data
|
||||
var data = JsonConvert.DeserializeObject<PosAppData>(e.Metadata.PosData);
|
||||
|
@ -370,6 +346,22 @@ namespace BTCPayServer.Services.Apps
|
|||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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
|
||||
});
|
||||
}
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
@using BTCPayServer.Components.StoreLightningBalance
|
||||
@using BTCPayServer.Components.StoreLightningServices
|
||||
@using BTCPayServer.Components.StoreNumbers
|
||||
@using BTCPayServer.Components.StoreRecentInvoices
|
||||
@using BTCPayServer.Components.StoreRecentTransactions
|
||||
@using BTCPayServer.Components.StoreWalletBalance
|
||||
@using BTCPayServer.TagHelpers
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Components.AppSales
|
||||
@using BTCPayServer.Components.AppTopItems
|
||||
@model StoreDashboardViewModel;
|
||||
|
||||
@inject BTCPayNetworkProvider networkProvider
|
||||
@{
|
||||
ViewData.SetActivePage(StoreNavPages.Dashboard, Model.StoreName, Model.StoreId);
|
||||
var defaultCryptoCode = networkProvider.DefaultNetwork.CryptoCode;
|
||||
var store = ViewContext.HttpContext.GetStoreData();
|
||||
}
|
||||
|
||||
|
@ -101,7 +104,7 @@
|
|||
<h5 class="mb-0 text-success">Set up a Lightning node</h5>
|
||||
</div>
|
||||
</div>
|
||||
<a asp-controller="UIStores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@defaultCryptoCode" id="SetupGuide-Wallet" class="list-group-item list-group-item-action d-flex align-items-center">
|
||||
<a asp-controller="UIStores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" id="SetupGuide-Wallet" class="list-group-item list-group-item-action d-flex align-items-center">
|
||||
<vc:icon symbol="new-wallet"/>
|
||||
<div class="content">
|
||||
<h5 class="mb-0">Set up a wallet</h5>
|
||||
|
@ -111,21 +114,21 @@
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
<vc:store-numbers store="@store"/>
|
||||
<vc:store-numbers vm="@(new StoreNumbersViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })"/>
|
||||
@if (Model.LightningEnabled)
|
||||
{
|
||||
<vc:store-lightning-balance vm="@(new StoreLightningBalanceViewModel { Store = store, InitialRendering = true })"/>
|
||||
<vc:store-lightning-services store="@store"/>
|
||||
<vc:store-lightning-balance vm="@(new StoreLightningBalanceViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })"/>
|
||||
<vc:store-lightning-services vm="@(new StoreLightningServicesViewModel { Store = store, CryptoCode = Model.CryptoCode })"/>
|
||||
}
|
||||
@if (Model.WalletEnabled)
|
||||
{
|
||||
<vc:store-recent-transactions store="@store"/>
|
||||
<vc:store-recent-transactions vm="@(new StoreRecentTransactionsViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })"/>
|
||||
}
|
||||
<vc:store-recent-invoices store="@store"/>
|
||||
<vc:store-recent-invoices vm="@(new StoreRecentInvoicesViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })"/>
|
||||
@foreach (var app in Model.Apps)
|
||||
{
|
||||
<vc:app-sales app="@app"/>
|
||||
<vc:app-top-items app="@app"/>
|
||||
<vc:app-sales vm="@(new AppSalesViewModel { App = app, InitialRendering = true })"/>
|
||||
<vc:app-top-items vm="@(new AppTopItemsViewModel { App = app, InitialRendering = true })"/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
@ -142,7 +145,7 @@ else
|
|||
</div>
|
||||
@if (!Model.WalletEnabled)
|
||||
{
|
||||
<a asp-controller="UIStores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@defaultCryptoCode" id="SetupGuide-Wallet" class="list-group-item list-group-item-action d-flex align-items-center order-1">
|
||||
<a asp-controller="UIStores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" id="SetupGuide-Wallet" class="list-group-item list-group-item-action d-flex align-items-center order-1">
|
||||
<vc:icon symbol="new-wallet"/>
|
||||
<div class="content">
|
||||
<h5 class="mb-0">Set up a wallet</h5>
|
||||
|
@ -161,7 +164,7 @@ else
|
|||
}
|
||||
@if (!Model.LightningEnabled)
|
||||
{
|
||||
<a asp-controller="UIStores" asp-action="SetupLightningNode" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@defaultCryptoCode" id="SetupGuide-Lightning" class="list-group-item list-group-item-action d-flex align-items-center order-1">
|
||||
<a asp-controller="UIStores" asp-action="SetupLightningNode" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" id="SetupGuide-Lightning" class="list-group-item list-group-item-action d-flex align-items-center order-1">
|
||||
<vc:icon symbol="new-wallet"/>
|
||||
<div class="content">
|
||||
<h5 class="mb-0">Set up a Lightning node</h5>
|
||||
|
|
Loading…
Add table
Reference in a new issue