mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
add in more info and simplify backend model
This commit is contained in:
@@ -1,22 +1,9 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
using BTCPayServer.Data;
|
|
||||||
using BTCPayServer.Events;
|
|
||||||
using BTCPayServer.Models.AppViewModels;
|
using BTCPayServer.Models.AppViewModels;
|
||||||
using BTCPayServer.Payments;
|
|
||||||
using BTCPayServer.Rating;
|
|
||||||
using BTCPayServer.Services.Apps;
|
|
||||||
using BTCPayServer.Services.Invoices;
|
|
||||||
using BTCPayServer.Services.Rates;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Hubs
|
namespace BTCPayServer.Hubs
|
||||||
{
|
{
|
||||||
@@ -52,232 +39,4 @@ namespace BTCPayServer.Hubs
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CrowdfundHubStreamer
|
|
||||||
{
|
|
||||||
public const string CrowdfundInvoiceOrderIdPrefix = "crowdfund-app:";
|
|
||||||
private readonly EventAggregator _EventAggregator;
|
|
||||||
private readonly IHubContext<CrowdfundHub> _HubContext;
|
|
||||||
private readonly IMemoryCache _MemoryCache;
|
|
||||||
private readonly AppsHelper _AppsHelper;
|
|
||||||
private readonly RateFetcher _RateFetcher;
|
|
||||||
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
|
||||||
private readonly InvoiceRepository _InvoiceRepository;
|
|
||||||
|
|
||||||
private Dictionary<string, CancellationTokenSource> _CacheTokens = new Dictionary<string, CancellationTokenSource>();
|
|
||||||
public CrowdfundHubStreamer(EventAggregator eventAggregator,
|
|
||||||
IHubContext<CrowdfundHub> hubContext,
|
|
||||||
IMemoryCache memoryCache,
|
|
||||||
AppsHelper appsHelper,
|
|
||||||
RateFetcher rateFetcher,
|
|
||||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
|
||||||
InvoiceRepository invoiceRepository)
|
|
||||||
{
|
|
||||||
_EventAggregator = eventAggregator;
|
|
||||||
_HubContext = hubContext;
|
|
||||||
_MemoryCache = memoryCache;
|
|
||||||
_AppsHelper = appsHelper;
|
|
||||||
_RateFetcher = rateFetcher;
|
|
||||||
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
|
||||||
_InvoiceRepository = invoiceRepository;
|
|
||||||
SubscribeToEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<ViewCrowdfundViewModel> GetCrowdfundInfo(string appId)
|
|
||||||
{
|
|
||||||
var key = GetCacheKey(appId);
|
|
||||||
return _MemoryCache.GetOrCreateAsync(key, async entry =>
|
|
||||||
{
|
|
||||||
if (_CacheTokens.ContainsKey(key))
|
|
||||||
{
|
|
||||||
_CacheTokens.Remove(key);
|
|
||||||
}
|
|
||||||
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
|
|
||||||
var result = await GetInfo(app);
|
|
||||||
entry.SetValue(result);
|
|
||||||
|
|
||||||
var token = new CancellationTokenSource();
|
|
||||||
_CacheTokens.Add(key, token);
|
|
||||||
entry.AddExpirationToken(new CancellationChangeToken(token.Token));
|
|
||||||
TimeSpan? expire = null;
|
|
||||||
|
|
||||||
if (result.StartDate.HasValue && result.StartDate < DateTime.Now)
|
|
||||||
{
|
|
||||||
expire = result.StartDate.Value.Subtract(DateTime.Now);
|
|
||||||
}
|
|
||||||
else if (result.EndDate.HasValue && result.EndDate > DateTime.Now)
|
|
||||||
{
|
|
||||||
expire = result.EndDate.Value.Subtract(DateTime.Now);
|
|
||||||
}
|
|
||||||
if(!expire.HasValue || expire?.TotalMinutes > 5)
|
|
||||||
{
|
|
||||||
expire = TimeSpan.FromMinutes(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.AbsoluteExpirationRelativeToNow = expire;
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SubscribeToEvents()
|
|
||||||
{
|
|
||||||
|
|
||||||
_EventAggregator.Subscribe<InvoiceEvent>(Subscription);
|
|
||||||
_EventAggregator.Subscribe<AppsController.CrowdfundAppUpdated>(updated =>
|
|
||||||
{
|
|
||||||
InvalidateCacheForApp(updated.AppId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetCacheKey(string appId)
|
|
||||||
{
|
|
||||||
return $"{CrowdfundInvoiceOrderIdPrefix}:{appId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Subscription(InvoiceEvent invoiceEvent)
|
|
||||||
{
|
|
||||||
if (!invoiceEvent.Invoice.OrderId.StartsWith(CrowdfundInvoiceOrderIdPrefix, StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var appId = invoiceEvent.Invoice.OrderId.Replace(CrowdfundInvoiceOrderIdPrefix, "", StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
switch (invoiceEvent.Name)
|
|
||||||
{
|
|
||||||
case InvoiceEvent.ReceivedPayment:
|
|
||||||
|
|
||||||
_HubContext.Clients.Group(appId).SendCoreAsync(CrowdfundHub.PaymentReceived, new object[]
|
|
||||||
{
|
|
||||||
invoiceEvent.Payment.GetCryptoPaymentData().GetValue(),
|
|
||||||
invoiceEvent.Payment.GetCryptoCode(),
|
|
||||||
Enum.GetName(typeof(PaymentTypes),
|
|
||||||
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
|
|
||||||
} );
|
|
||||||
|
|
||||||
InvalidateCacheForApp(appId);
|
|
||||||
break;
|
|
||||||
case InvoiceEvent.Completed:
|
|
||||||
InvalidateCacheForApp(appId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InvalidateCacheForApp(string appId)
|
|
||||||
{
|
|
||||||
if (_CacheTokens.ContainsKey(appId))
|
|
||||||
{
|
|
||||||
_CacheTokens[appId].Cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
GetCrowdfundInfo(appId).ContinueWith(task =>
|
|
||||||
{
|
|
||||||
_HubContext.Clients.Group(appId).SendCoreAsync(CrowdfundHub.InfoUpdated, new object[]{ task.Result} );
|
|
||||||
}, TaskScheduler.Default);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<decimal> GetCurrentContributionAmount(InvoiceEntity[] invoices, string primaryCurrency,
|
|
||||||
RateFetcher rateFetcher, RateRules rateRules)
|
|
||||||
{
|
|
||||||
decimal result = 0;
|
|
||||||
|
|
||||||
var groupingByCurrency = invoices.GroupBy(entity => entity.ProductInformation.Currency);
|
|
||||||
|
|
||||||
var ratesTask = rateFetcher.FetchRates(
|
|
||||||
groupingByCurrency
|
|
||||||
.Select((entities) => new CurrencyPair(entities.Key, primaryCurrency))
|
|
||||||
.ToHashSet(),
|
|
||||||
rateRules);
|
|
||||||
|
|
||||||
var finalTasks = new List<Task>();
|
|
||||||
foreach (var rateTask in ratesTask)
|
|
||||||
{
|
|
||||||
finalTasks.Add(Task.Run(async () =>
|
|
||||||
{
|
|
||||||
var tResult = await rateTask.Value;
|
|
||||||
var rate = tResult.BidAsk?.Bid;
|
|
||||||
if (rate == null) return;
|
|
||||||
var currencyGroup = groupingByCurrency.Single(entities => entities.Key == rateTask.Key.Left);
|
|
||||||
result += currencyGroup.Sum(entity => entity.ProductInformation.Price / rate.Value);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.WhenAll(finalTasks);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, string statusMessage= null)
|
|
||||||
{
|
|
||||||
var settings = appData.GetSettings<AppsController.CrowdfundSettings>();
|
|
||||||
var invoices = await GetInvoicesForApp(appData, _InvoiceRepository);
|
|
||||||
|
|
||||||
|
|
||||||
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
|
|
||||||
var currentAmount = await GetCurrentContributionAmount(
|
|
||||||
invoices.Where(entity => entity.Status == InvoiceStatus.Complete).ToArray(),
|
|
||||||
settings.TargetCurrency, _RateFetcher, rateRules);
|
|
||||||
var currentPendingAmount = await GetCurrentContributionAmount(
|
|
||||||
invoices.Where(entity => entity.Status != InvoiceStatus.Complete).ToArray(),
|
|
||||||
settings.TargetCurrency, _RateFetcher, rateRules);
|
|
||||||
|
|
||||||
|
|
||||||
var active = (settings.StartDate == null || DateTime.Now >= settings.StartDate) &&
|
|
||||||
(settings.EndDate == null || DateTime.Now <= settings.EndDate) &&
|
|
||||||
(!settings.EnforceTargetAmount || settings.TargetAmount > currentAmount);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return new ViewCrowdfundViewModel()
|
|
||||||
{
|
|
||||||
Title = settings.Title,
|
|
||||||
Tagline = settings.Tagline,
|
|
||||||
Description = settings.Description,
|
|
||||||
CustomCSSLink = settings.CustomCSSLink,
|
|
||||||
MainImageUrl = settings.MainImageUrl,
|
|
||||||
EmbeddedCSS = settings.EmbeddedCSS,
|
|
||||||
StoreId = appData.StoreDataId,
|
|
||||||
AppId = appData.Id,
|
|
||||||
StartDate = settings.StartDate,
|
|
||||||
EndDate = settings.EndDate,
|
|
||||||
TargetAmount = settings.TargetAmount,
|
|
||||||
TargetCurrency = settings.TargetCurrency,
|
|
||||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
|
||||||
StatusMessage = statusMessage,
|
|
||||||
Perks = _AppsHelper.Parse(settings.PerksTemplate, settings.TargetCurrency),
|
|
||||||
DisqusEnabled = settings.DisqusEnabled,
|
|
||||||
SoundsEnabled = settings.SoundsEnabled,
|
|
||||||
DisqusShortname = settings.DisqusShortname,
|
|
||||||
AnimationsEnabled = settings.AnimationsEnabled,
|
|
||||||
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
|
|
||||||
{
|
|
||||||
TotalContributors = invoices.Length,
|
|
||||||
CurrentPendingAmount = currentPendingAmount,
|
|
||||||
CurrentAmount = currentAmount,
|
|
||||||
Active = active,
|
|
||||||
DaysLeft = settings.EndDate.HasValue? (settings.EndDate - DateTime.UtcNow).Value.Days: (int?) null,
|
|
||||||
DaysLeftToStart = settings.StartDate.HasValue? (settings.StartDate - DateTime.UtcNow).Value.Days: (int?) null,
|
|
||||||
ShowProgress = settings.TargetAmount.HasValue,
|
|
||||||
ProgressPercentage = (currentAmount/ settings.TargetAmount) * 100,
|
|
||||||
PendingProgressPercentage = ( currentPendingAmount/ settings.TargetAmount) * 100,
|
|
||||||
LastUpdated = DateTime.Now
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, InvoiceRepository invoiceRepository)
|
|
||||||
{
|
|
||||||
return await invoiceRepository.GetInvoices(new InvoiceQuery()
|
|
||||||
{
|
|
||||||
OrderId = $"{CrowdfundInvoiceOrderIdPrefix}{appData.Id}",
|
|
||||||
Status = new string[]{
|
|
||||||
InvoiceState.ToString(InvoiceStatus.New),
|
|
||||||
InvoiceState.ToString(InvoiceStatus.Paid),
|
|
||||||
InvoiceState.ToString(InvoiceStatus.Confirmed),
|
|
||||||
InvoiceState.ToString(InvoiceStatus.Complete)}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
253
BTCPayServer/Crowdfund/CrowdfundHubStreamer.cs
Normal file
253
BTCPayServer/Crowdfund/CrowdfundHubStreamer.cs
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Controllers;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Events;
|
||||||
|
using BTCPayServer.Models.AppViewModels;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
using BTCPayServer.Services.Apps;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Hubs
|
||||||
|
{
|
||||||
|
public class CrowdfundHubStreamer
|
||||||
|
{
|
||||||
|
public const string CrowdfundInvoiceOrderIdPrefix = "crowdfund-app:";
|
||||||
|
private readonly EventAggregator _EventAggregator;
|
||||||
|
private readonly IHubContext<CrowdfundHub> _HubContext;
|
||||||
|
private readonly IMemoryCache _MemoryCache;
|
||||||
|
private readonly AppsHelper _AppsHelper;
|
||||||
|
private readonly RateFetcher _RateFetcher;
|
||||||
|
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||||
|
private readonly InvoiceRepository _InvoiceRepository;
|
||||||
|
|
||||||
|
private Dictionary<string, CancellationTokenSource> _CacheTokens = new Dictionary<string, CancellationTokenSource>();
|
||||||
|
public CrowdfundHubStreamer(EventAggregator eventAggregator,
|
||||||
|
IHubContext<CrowdfundHub> hubContext,
|
||||||
|
IMemoryCache memoryCache,
|
||||||
|
AppsHelper appsHelper,
|
||||||
|
RateFetcher rateFetcher,
|
||||||
|
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||||
|
InvoiceRepository invoiceRepository)
|
||||||
|
{
|
||||||
|
_EventAggregator = eventAggregator;
|
||||||
|
_HubContext = hubContext;
|
||||||
|
_MemoryCache = memoryCache;
|
||||||
|
_AppsHelper = appsHelper;
|
||||||
|
_RateFetcher = rateFetcher;
|
||||||
|
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||||
|
_InvoiceRepository = invoiceRepository;
|
||||||
|
SubscribeToEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ViewCrowdfundViewModel> GetCrowdfundInfo(string appId)
|
||||||
|
{
|
||||||
|
var key = GetCacheKey(appId);
|
||||||
|
return _MemoryCache.GetOrCreateAsync(key, async entry =>
|
||||||
|
{
|
||||||
|
if (_CacheTokens.ContainsKey(key))
|
||||||
|
{
|
||||||
|
_CacheTokens.Remove(key);
|
||||||
|
}
|
||||||
|
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
|
||||||
|
var result = await GetInfo(app);
|
||||||
|
entry.SetValue(result);
|
||||||
|
|
||||||
|
var token = new CancellationTokenSource();
|
||||||
|
_CacheTokens.Add(key, token);
|
||||||
|
entry.AddExpirationToken(new CancellationChangeToken(token.Token));
|
||||||
|
TimeSpan? expire = null;
|
||||||
|
|
||||||
|
if (result.StartDate.HasValue && result.StartDate < DateTime.Now)
|
||||||
|
{
|
||||||
|
expire = result.StartDate.Value.Subtract(DateTime.Now);
|
||||||
|
}
|
||||||
|
else if (result.EndDate.HasValue && result.EndDate > DateTime.Now)
|
||||||
|
{
|
||||||
|
expire = result.EndDate.Value.Subtract(DateTime.Now);
|
||||||
|
}
|
||||||
|
if(!expire.HasValue || expire?.TotalMinutes > 5 || expire?.TotalMilliseconds <= 0)
|
||||||
|
{
|
||||||
|
expire = TimeSpan.FromMinutes(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.AbsoluteExpirationRelativeToNow = expire;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SubscribeToEvents()
|
||||||
|
{
|
||||||
|
|
||||||
|
_EventAggregator.Subscribe<InvoiceEvent>(Subscription);
|
||||||
|
_EventAggregator.Subscribe<AppsController.CrowdfundAppUpdated>(updated =>
|
||||||
|
{
|
||||||
|
InvalidateCacheForApp(updated.AppId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCacheKey(string appId)
|
||||||
|
{
|
||||||
|
return $"{CrowdfundInvoiceOrderIdPrefix}:{appId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Subscription(InvoiceEvent invoiceEvent)
|
||||||
|
{
|
||||||
|
if (!invoiceEvent.Invoice.OrderId.StartsWith(CrowdfundInvoiceOrderIdPrefix, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var appId = invoiceEvent.Invoice.OrderId.Replace(CrowdfundInvoiceOrderIdPrefix, "", StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
switch (invoiceEvent.Name)
|
||||||
|
{
|
||||||
|
case InvoiceEvent.ReceivedPayment:
|
||||||
|
|
||||||
|
_HubContext.Clients.Group(appId).SendCoreAsync(CrowdfundHub.PaymentReceived, new object[]
|
||||||
|
{
|
||||||
|
invoiceEvent.Payment.GetCryptoPaymentData().GetValue(),
|
||||||
|
invoiceEvent.Payment.GetCryptoCode(),
|
||||||
|
Enum.GetName(typeof(PaymentTypes),
|
||||||
|
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
|
||||||
|
} );
|
||||||
|
|
||||||
|
InvalidateCacheForApp(appId);
|
||||||
|
break;
|
||||||
|
case InvoiceEvent.Completed:
|
||||||
|
InvalidateCacheForApp(appId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InvalidateCacheForApp(string appId)
|
||||||
|
{
|
||||||
|
if (_CacheTokens.ContainsKey(appId))
|
||||||
|
{
|
||||||
|
_CacheTokens[appId].Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
GetCrowdfundInfo(appId).ContinueWith(task =>
|
||||||
|
{
|
||||||
|
_HubContext.Clients.Group(appId).SendCoreAsync(CrowdfundHub.InfoUpdated, new object[]{ task.Result} );
|
||||||
|
}, TaskScheduler.Default);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<decimal> GetCurrentContributionAmount(InvoiceEntity[] invoices, string primaryCurrency,
|
||||||
|
RateFetcher rateFetcher, RateRules rateRules)
|
||||||
|
{
|
||||||
|
decimal result = 0;
|
||||||
|
|
||||||
|
var groupingByCurrency = invoices.GroupBy(entity => entity.ProductInformation.Currency);
|
||||||
|
|
||||||
|
var ratesTask = rateFetcher.FetchRates(
|
||||||
|
groupingByCurrency
|
||||||
|
.Select((entities) => new CurrencyPair(entities.Key, primaryCurrency))
|
||||||
|
.ToHashSet(),
|
||||||
|
rateRules);
|
||||||
|
|
||||||
|
var finalTasks = new List<Task>();
|
||||||
|
foreach (var rateTask in ratesTask)
|
||||||
|
{
|
||||||
|
finalTasks.Add(Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var tResult = await rateTask.Value;
|
||||||
|
var rate = tResult.BidAsk?.Bid;
|
||||||
|
if (rate == null) return;
|
||||||
|
var currencyGroup = groupingByCurrency.Single(entities => entities.Key == rateTask.Key.Left);
|
||||||
|
result += currencyGroup.Sum(entity => entity.ProductInformation.Price / rate.Value);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(finalTasks);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, decimal> GetCurrentContributionAmountStats(InvoiceEntity[] invoices)
|
||||||
|
{
|
||||||
|
var payments = invoices.SelectMany(entity => entity.GetPayments());
|
||||||
|
|
||||||
|
var groupedByMethod = payments.GroupBy(entity => entity.GetPaymentMethodId());
|
||||||
|
|
||||||
|
return groupedByMethod.ToDictionary(entities => entities.Key.ToString(),
|
||||||
|
entities => entities.Sum(entity => entity.GetCryptoPaymentData().GetValue()));
|
||||||
|
}
|
||||||
|
private async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, string statusMessage= null)
|
||||||
|
{
|
||||||
|
var settings = appData.GetSettings<AppsController.CrowdfundSettings>();
|
||||||
|
var invoices = await GetInvoicesForApp(appData, _InvoiceRepository);
|
||||||
|
|
||||||
|
var completeInvoices = invoices.Where(entity => entity.Status == InvoiceStatus.Complete).ToArray();
|
||||||
|
var pendingInvoices = invoices.Where(entity => entity.Status != InvoiceStatus.Complete).ToArray();
|
||||||
|
|
||||||
|
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
|
||||||
|
|
||||||
|
var currentAmount = await GetCurrentContributionAmount(
|
||||||
|
completeInvoices,
|
||||||
|
settings.TargetCurrency, _RateFetcher, rateRules);
|
||||||
|
var currentPendingAmount = await GetCurrentContributionAmount(
|
||||||
|
pendingInvoices,
|
||||||
|
settings.TargetCurrency, _RateFetcher, rateRules);
|
||||||
|
|
||||||
|
var pendingPaymentStats = GetCurrentContributionAmountStats(pendingInvoices);
|
||||||
|
var paymentStats = GetCurrentContributionAmountStats(completeInvoices);
|
||||||
|
|
||||||
|
return new ViewCrowdfundViewModel()
|
||||||
|
{
|
||||||
|
Title = settings.Title,
|
||||||
|
Tagline = settings.Tagline,
|
||||||
|
Description = settings.Description,
|
||||||
|
CustomCSSLink = settings.CustomCSSLink,
|
||||||
|
MainImageUrl = settings.MainImageUrl,
|
||||||
|
EmbeddedCSS = settings.EmbeddedCSS,
|
||||||
|
StoreId = appData.StoreDataId,
|
||||||
|
AppId = appData.Id,
|
||||||
|
StartDate = settings.StartDate,
|
||||||
|
EndDate = settings.EndDate,
|
||||||
|
TargetAmount = settings.TargetAmount,
|
||||||
|
TargetCurrency = settings.TargetCurrency,
|
||||||
|
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||||
|
StatusMessage = statusMessage,
|
||||||
|
Perks = _AppsHelper.Parse(settings.PerksTemplate, settings.TargetCurrency),
|
||||||
|
DisqusEnabled = settings.DisqusEnabled,
|
||||||
|
SoundsEnabled = settings.SoundsEnabled,
|
||||||
|
DisqusShortname = settings.DisqusShortname,
|
||||||
|
AnimationsEnabled = settings.AnimationsEnabled,
|
||||||
|
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
|
||||||
|
{
|
||||||
|
TotalContributors = invoices.Length,
|
||||||
|
CurrentPendingAmount = currentPendingAmount,
|
||||||
|
CurrentAmount = currentAmount,
|
||||||
|
ProgressPercentage = (currentAmount/ settings.TargetAmount) * 100,
|
||||||
|
PendingProgressPercentage = ( currentPendingAmount/ settings.TargetAmount) * 100,
|
||||||
|
LastUpdated = DateTime.Now,
|
||||||
|
PaymentStats = paymentStats,
|
||||||
|
PendingPaymentStats = pendingPaymentStats
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, InvoiceRepository invoiceRepository)
|
||||||
|
{
|
||||||
|
return await invoiceRepository.GetInvoices(new InvoiceQuery()
|
||||||
|
{
|
||||||
|
OrderId = $"{CrowdfundInvoiceOrderIdPrefix}{appData.Id}",
|
||||||
|
Status = new string[]{
|
||||||
|
InvoiceState.ToString(InvoiceStatus.New),
|
||||||
|
InvoiceState.ToString(InvoiceStatus.Paid),
|
||||||
|
InvoiceState.ToString(InvoiceStatus.Confirmed),
|
||||||
|
InvoiceState.ToString(InvoiceStatus.Complete)}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.AppViewModels
|
namespace BTCPayServer.Models.AppViewModels
|
||||||
{
|
{
|
||||||
@@ -32,20 +34,16 @@ namespace BTCPayServer.Models.AppViewModels
|
|||||||
public class CrowdfundInfo
|
public class CrowdfundInfo
|
||||||
{
|
{
|
||||||
public int TotalContributors { get; set; }
|
public int TotalContributors { get; set; }
|
||||||
public decimal CurrentAmount { get; set; }
|
|
||||||
public bool Active { get; set; }
|
|
||||||
public bool ShowProgress { get; set; }
|
|
||||||
public decimal? ProgressPercentage { get; set; }
|
|
||||||
public int? DaysLeft{ get; set; }
|
|
||||||
public int? DaysLeftToStart{ get; set; }
|
|
||||||
public decimal CurrentPendingAmount { get; set; }
|
public decimal CurrentPendingAmount { get; set; }
|
||||||
|
public decimal CurrentAmount { get; set; }
|
||||||
|
public decimal? ProgressPercentage { get; set; }
|
||||||
public decimal? PendingProgressPercentage { get; set; }
|
public decimal? PendingProgressPercentage { get; set; }
|
||||||
public DateTime LastUpdated { get; set; }
|
public DateTime LastUpdated { get; set; }
|
||||||
|
public Dictionary<string, decimal> PaymentStats { get; set; }
|
||||||
|
public Dictionary<string, decimal> PendingPaymentStats { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public class ContributeToCrowdfund
|
public class ContributeToCrowdfund
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,101 +1,101 @@
|
|||||||
@using BTCPayServer.Models.AppViewModels
|
@* @using BTCPayServer.Models.AppViewModels *@
|
||||||
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
|
@* @model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel *@
|
||||||
<div class="container p-0" >
|
@* <div class="container p-0" > *@
|
||||||
|
@* *@
|
||||||
|
@* *@
|
||||||
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
|
@* <div class="row h-100 w-100 py-sm-0 py-md-4 mx-0"> *@
|
||||||
|
@* *@
|
||||||
<div class="card w-100 p-0 mx-0">
|
@* <div class="card w-100 p-0 mx-0"> *@
|
||||||
<partial name="_StatusMessage" for="@Model.StatusMessage"/>
|
@* <partial name="_StatusMessage" for="@Model.StatusMessage"/> *@
|
||||||
@if (!string.IsNullOrEmpty(Model.MainImageUrl))
|
@* @if (!string.IsNullOrEmpty(Model.MainImageUrl)) *@
|
||||||
{
|
@* { *@
|
||||||
<img class="card-img-top" src="@Model.MainImageUrl" alt="Card image cap">
|
@* <img class="card-img-top" src="@Model.MainImageUrl" alt="Card image cap"> *@
|
||||||
}
|
@* } *@
|
||||||
@if (Model.Info.ShowProgress)
|
@* @if (Model.Info.ShowProgress) *@
|
||||||
{
|
@* { *@
|
||||||
<div class="progress rounded-0 striped" style="min-height: 30px">
|
@* <div class="progress rounded-0 striped" style="min-height: 30px"> *@
|
||||||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="@Model.Info.ProgressPercentage" aria-valuemin="0" aria-valuemax="100">
|
@* <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="@Model.Info.ProgressPercentage" aria-valuemin="0" aria-valuemax="100"> *@
|
||||||
@if (Model.Info.ProgressPercentage.Value > 0)
|
@* @if (Model.Info.ProgressPercentage.Value > 0) *@
|
||||||
{
|
@* { *@
|
||||||
@(Model.Info.ProgressPercentage + "%")
|
@* @(Model.Info.ProgressPercentage + "%") *@
|
||||||
}
|
@* } *@
|
||||||
</div>
|
@* </div> *@
|
||||||
</div>
|
@* </div> *@
|
||||||
}
|
@* } *@
|
||||||
<div class="card-body">
|
@* <div class="card-body"> *@
|
||||||
<div class="card-title row">
|
@* <div class="card-title row"> *@
|
||||||
<div class="col-md-9 col-sm-12">
|
@* <div class="col-md-9 col-sm-12"> *@
|
||||||
|
@* *@
|
||||||
<h1 >
|
@* <h1 > *@
|
||||||
@Model.Title
|
@* @Model.Title *@
|
||||||
@if (!string.IsNullOrEmpty(Model.Tagline))
|
@* @if (!string.IsNullOrEmpty(Model.Tagline)) *@
|
||||||
{
|
@* { *@
|
||||||
<h2 class="text-muted">@Model.Tagline</h2>
|
@* <h2 class="text-muted">@Model.Tagline</h2> *@
|
||||||
}
|
@* } *@
|
||||||
|
@* *@
|
||||||
@if (Model.Info.DaysLeftToStart.HasValue && Model.Info.DaysLeftToStart > 0)
|
@* @if (Model.Info.DaysLeftToStart.HasValue && Model.Info.DaysLeftToStart > 0) *@
|
||||||
{
|
@* { *@
|
||||||
<small>
|
@* <small> *@
|
||||||
@($"{Model.Info.DaysLeftToStart} day{(Model.Info.DaysLeftToStart.Value > 1 ? "s" : "")} left to start")
|
@* @($"{Model.Info.DaysLeftToStart} day{(Model.Info.DaysLeftToStart.Value > 1 ? "s" : "")} left to start") *@
|
||||||
|
@* *@
|
||||||
</small>
|
@* </small> *@
|
||||||
}
|
@* } *@
|
||||||
</h1>
|
@* </h1> *@
|
||||||
|
@* *@
|
||||||
</div>
|
@* </div> *@
|
||||||
<ul class="list-group list-group-flush col-md-3 col-sm-12">
|
@* <ul class="list-group list-group-flush col-md-3 col-sm-12"> *@
|
||||||
<li class="list-group-item">@(Model.EndDate.HasValue? $"Ends {Model.EndDate.Value:dddd, dd MMMM yyyy HH:mm}" : "No specific end date")</li>
|
@* <li class="list-group-item">@(Model.EndDate.HasValue? $"Ends {Model.EndDate.Value:dddd, dd MMMM yyyy HH:mm}" : "No specific end date")</li> *@
|
||||||
<li class="list-group-item">@(Model.TargetAmount.HasValue? $"{Model.TargetAmount:G29} {Model.TargetCurrency.ToUpperInvariant()} Goal" :
|
@* <li class="list-group-item">@(Model.TargetAmount.HasValue? $"{Model.TargetAmount:G29} {Model.TargetCurrency.ToUpperInvariant()} Goal" : *@
|
||||||
"No specific target goal")</li>
|
@* "No specific target goal")</li> *@
|
||||||
<li class="list-group-item">@(Model.EnforceTargetAmount? $"Hardcap Goal" : "Softcap Goal")</li>
|
@* <li class="list-group-item">@(Model.EnforceTargetAmount? $"Hardcap Goal" : "Softcap Goal")</li> *@
|
||||||
</ul>
|
@* </ul> *@
|
||||||
|
@* *@
|
||||||
</div>
|
@* </div> *@
|
||||||
@if (Model.Info.Active)
|
@* @if (Model.Info.Active) *@
|
||||||
{
|
@* { *@
|
||||||
<div class="card-deck mb-4 ">
|
@* <div class="card-deck mb-4 "> *@
|
||||||
<div class="card shadow">
|
@* <div class="card shadow"> *@
|
||||||
<div class="card-body">
|
@* <div class="card-body"> *@
|
||||||
<h5 class="card-title text-center">@Model.Info.TotalContributors</h5>
|
@* <h5 class="card-title text-center">@Model.Info.TotalContributors</h5> *@
|
||||||
<h6 class="card-text text-center"> Contributors</h6>
|
@* <h6 class="card-text text-center"> Contributors</h6> *@
|
||||||
</div>
|
@* </div> *@
|
||||||
</div>
|
@* </div> *@
|
||||||
<div class="card shadow">
|
@* <div class="card shadow"> *@
|
||||||
<div class="card-body">
|
@* <div class="card-body"> *@
|
||||||
<h5 class="card-title text-center">@Model.Info.CurrentAmount @Model.TargetCurrency.ToUpperInvariant()</h5>
|
@* <h5 class="card-title text-center">@Model.Info.CurrentAmount @Model.TargetCurrency.ToUpperInvariant()</h5> *@
|
||||||
<h6 class="card-text text-center"> Raised</h6>
|
@* <h6 class="card-text text-center"> Raised</h6> *@
|
||||||
</div>
|
@* </div> *@
|
||||||
</div>
|
@* </div> *@
|
||||||
|
@* *@
|
||||||
@if (Model.Info.DaysLeft.HasValue && Model.Info.DaysLeft > 0)
|
@* @if (Model.Info.DaysLeft.HasValue && Model.Info.DaysLeft > 0) *@
|
||||||
{
|
@* { *@
|
||||||
<div class="card shadow">
|
@* <div class="card shadow"> *@
|
||||||
<div class="card-body">
|
@* <div class="card-body"> *@
|
||||||
<h5 class="card-title text-center">@Model.Info.DaysLeft</h5>
|
@* <h5 class="card-title text-center">@Model.Info.DaysLeft</h5> *@
|
||||||
<h6 class="card-text text-center">Day@(Model.Info.DaysLeft.Value > 1 ? "s" : "") left</h6>
|
@* <h6 class="card-text text-center">Day@(Model.Info.DaysLeft.Value > 1 ? "s" : "") left</h6> *@
|
||||||
</div>
|
@* </div> *@
|
||||||
</div>
|
@* </div> *@
|
||||||
}
|
@* } *@
|
||||||
|
@* *@
|
||||||
|
@* *@
|
||||||
</div>
|
@* </div> *@
|
||||||
}
|
@* } *@
|
||||||
|
@* *@
|
||||||
|
@* *@
|
||||||
<div class="card-text"> @Html.Raw(Model.Description)</div>
|
@* <div class="card-text"> @Html.Raw(Model.Description)</div> *@
|
||||||
@if (Model.Info.Active)
|
@* @if (Model.Info.Active) *@
|
||||||
{
|
@* { *@
|
||||||
<hr/>
|
@* <hr/> *@
|
||||||
<h3>Contribute</h3>
|
@* <h3>Contribute</h3> *@
|
||||||
<partial name="Crowdfund/ContributeForm" model="@(new ContributeToCrowdfund()
|
@* <partial name="Crowdfund/ContributeForm" model="@(new ContributeToCrowdfund() *@
|
||||||
{
|
@* { *@
|
||||||
RedirectToCheckout = true,
|
@* RedirectToCheckout = true, *@
|
||||||
ViewCrowdfundViewModel = Model
|
@* ViewCrowdfundViewModel = Model *@
|
||||||
})"/>
|
@* })"/> *@
|
||||||
}
|
@* } *@
|
||||||
</div>
|
@* </div> *@
|
||||||
</div>
|
@* </div> *@
|
||||||
|
@* *@
|
||||||
|
@* *@
|
||||||
</div>
|
@* </div> *@
|
||||||
</div>
|
@* </div> *@
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="progress w-100 rounded-0 " v-if="srvModel.info.showProgress">
|
<div class="progress w-100 rounded-0 " v-if="srvModel.targetAmount">
|
||||||
<div class="progress-bar" role="progressbar"
|
<div class="progress-bar" role="progressbar"
|
||||||
:aria-valuenow="srvModel.info.progressPercentage"
|
:aria-valuenow="srvModel.info.progressPercentage"
|
||||||
v-bind:style="{ width: srvModel.info.progressPercentage + '%' }"
|
v-bind:style="{ width: srvModel.info.progressPercentage + '%' }"
|
||||||
@@ -44,9 +44,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
|
|
||||||
<div class="row py-2 text-center">
|
<div class="row py-2 text-center">
|
||||||
<div class="col-sm border-right">
|
<div class="col-sm border-right" id="raised-amount">
|
||||||
<h5>{{srvModel.info.currentAmount + srvModel.info.currentPendingAmount }} {{targetCurrency}} </h5>
|
<h5>{{srvModel.info.currentAmount + srvModel.info.currentPendingAmount }} {{targetCurrency}} </h5>
|
||||||
<h5 class="text-muted">Raised</h5>
|
<h5 class="text-muted">Raised</h5>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,13 +73,34 @@
|
|||||||
</h5>
|
</h5>
|
||||||
<h5 class="text-muted">Left to start</h5>
|
<h5 class="text-muted">Left to start</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm" v-if="ended">
|
<div class="col-sm" v-if="ended" id="inactive-campaign">
|
||||||
<h5>
|
<h5>
|
||||||
Campaign
|
Campaign
|
||||||
</h5>
|
</h5>
|
||||||
<h5 >not active</h5>
|
<h5 >not active</h5>
|
||||||
|
|
||||||
|
<b-tooltip target="inactive-campaign" >
|
||||||
|
<ul class="p-0">
|
||||||
|
<li v-if="startDate" style="list-style-type: none">
|
||||||
|
{{started? "Started" : "Starts"}} {{startDate}}
|
||||||
|
</li>
|
||||||
|
<li v-if="endDate" style="list-style-type: none">
|
||||||
|
{{ended? "Ended" : "End"}} {{endDate}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</b-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<b-tooltip target="raised-amount" >
|
||||||
|
<ul class="p-0">
|
||||||
|
<li v-for="stat of paymentStats" style="list-style-type: none">
|
||||||
|
{{stat.label}} {{stat.value}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</b-tooltip>
|
||||||
|
|
||||||
|
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -165,7 +187,7 @@
|
|||||||
|
|
||||||
<script type="text/x-template" id="perks-template">
|
<script type="text/x-template" id="perks-template">
|
||||||
<div >
|
<div >
|
||||||
<perk :perk="{title: 'Donate Custom Amount', price: { value: null}, custom: true}" :target-currency="targetCurrency" :active="active"
|
<perk v-if="!perks || perks.length ===0" :perk="{title: 'Donate Custom Amount', price: { value: null}, custom: true}" :target-currency="targetCurrency" :active="active"
|
||||||
:in-modal="inModal">
|
:in-modal="inModal">
|
||||||
</perk>
|
</perk>
|
||||||
<perk v-for="(perk, index) in perks" :perk="perk" :target-currency="targetCurrency" :active="active"
|
<perk v-for="(perk, index) in perks" :perk="perk" :target-currency="targetCurrency" :active="active"
|
||||||
|
|||||||
@@ -23,7 +23,8 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||||
</script>
|
</script>
|
||||||
<bundle name="wwwroot/bundles/crowdfund-bundle.min.js"></bundle>
|
<bundle name="wwwroot/bundles/crowdfund-bundle-1.min.js"></bundle>
|
||||||
|
<bundle name="wwwroot/bundles/crowdfund-bundle-2.min.js"></bundle>
|
||||||
<bundle name="wwwroot/bundles/crowdfund-bundle.min.css"></bundle>
|
<bundle name="wwwroot/bundles/crowdfund-bundle.min.css"></bundle>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"outputFileName": "wwwroot/bundles/crowdfund.min.js",
|
"outputFileName": "wwwroot/bundles/crowdfund-bundle-1.min.js",
|
||||||
"inputFiles": [
|
"inputFiles": [
|
||||||
"wwwroot/vendor/vuejs/vue.min.js",
|
"wwwroot/vendor/vuejs/vue.min.js",
|
||||||
"wwwroot/vendor/babel-polyfill/polyfill.min.js",
|
"wwwroot/vendor/babel-polyfill/polyfill.min.js",
|
||||||
@@ -79,9 +79,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"outputFileName": "wwwroot/bundles/crowdfund-bundle.min.js",
|
"outputFileName": "wwwroot/bundles/crowdfund-bundle-2.min.js",
|
||||||
"inputFiles": [
|
"inputFiles": [
|
||||||
"wwwroot/bundles/crowdfund.min.js",
|
|
||||||
"wwwroot/vendor/moment/moment.js"
|
"wwwroot/vendor/moment/moment.js"
|
||||||
],
|
],
|
||||||
"minify": {
|
"minify": {
|
||||||
|
|||||||
@@ -75,13 +75,50 @@ addLoadEvent(function (ev) {
|
|||||||
contributeModalOpen: false,
|
contributeModalOpen: false,
|
||||||
endDiff: "",
|
endDiff: "",
|
||||||
startDiff: "",
|
startDiff: "",
|
||||||
active: true
|
active: true,
|
||||||
|
animation: true,
|
||||||
|
sound: true
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
targetCurrency: function(){
|
targetCurrency: function(){
|
||||||
return this.srvModel.targetCurrency.toUpperCase();
|
return this.srvModel.targetCurrency.toUpperCase();
|
||||||
|
},
|
||||||
|
paymentStats: function(){
|
||||||
|
var result= [];
|
||||||
|
|
||||||
|
var combinedStats = {};
|
||||||
|
|
||||||
|
|
||||||
|
var keys = Object.keys(this.srvModel.info.paymentStats);
|
||||||
|
|
||||||
|
for (var i = 0; i < keys.length; i++) {
|
||||||
|
if(combinedStats[keys[i]]){
|
||||||
|
combinedStats[keys[i]] +=this.srvModel.info.paymentStats[keys[i]];
|
||||||
|
}else{
|
||||||
|
combinedStats[keys[i]] =this.srvModel.info.paymentStats[keys[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys = Object.keys(this.srvModel.info.pendingPaymentStats);
|
||||||
|
|
||||||
|
for (var i = 0; i < keys.length; i++) {
|
||||||
|
if(combinedStats[keys[i]]){
|
||||||
|
combinedStats[keys[i]] +=this.srvModel.info.pendingPaymentStats[keys[i]];
|
||||||
|
}else{
|
||||||
|
combinedStats[keys[i]] =this.srvModel.info.pendingPaymentStats[keys[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys = Object.keys(combinedStats);
|
||||||
|
|
||||||
|
for (var i = 0; i < keys.length; i++) {
|
||||||
|
var newItem = {key:keys[i], value: combinedStats[keys[i]], label: keys[i].replace("_","")};
|
||||||
|
result.push(newItem);
|
||||||
|
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
Reference in New Issue
Block a user