2022-03-23 13:53:32 +03:00
|
|
|
import Service, {inject as service} from '@ember/service';
|
|
|
|
import moment from 'moment';
|
2022-03-24 17:32:34 +03:00
|
|
|
import {task} from 'ember-concurrency';
|
2022-03-23 11:51:53 +03:00
|
|
|
import {tracked} from '@glimmer/tracking';
|
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
/**
|
|
|
|
* @typedef MrrStat
|
|
|
|
* @type {Object}
|
|
|
|
* @property {string} date The date (YYYY-MM-DD) on which this MRR was recorded
|
|
|
|
* @property {number} mrr The MRR on this date
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef MemberCountStat
|
|
|
|
* @type {Object}
|
|
|
|
* @property {string} date The date (YYYY-MM-DD) on which these counts were recorded
|
|
|
|
* @property {number} paid Amount of paid members
|
|
|
|
* @property {number} free Amount of free members
|
|
|
|
* @property {number} comped Amount of comped members
|
|
|
|
* @property {number} newPaid Amount of new paid members
|
|
|
|
* @property {number} canceledPaid Amount of canceled paid members
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef MemberCounts
|
|
|
|
* @type {Object}
|
|
|
|
* @property {number} total Total amount of members
|
|
|
|
* @property {number} paid Amount of paid members
|
|
|
|
* @property {number} free Amount of free members
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @todo: THIS ONE IS TEMPORARY
|
|
|
|
* @typedef EmailOpenRateStat (Will be the same as post model probably)
|
|
|
|
* @type {Object}
|
|
|
|
* @property {string} id Post id
|
|
|
|
* @property {string} title Post title
|
|
|
|
* @property {?Object} Email model
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef PaidMembersByCadence
|
|
|
|
* @type {Object}
|
|
|
|
* @property {number} annual Paid memebrs on annual plan
|
|
|
|
* @property {number} monthly Paid memebrs on monthly plan
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef PaidMembersForTier
|
|
|
|
* @type {Object}
|
|
|
|
* @property {Object} tier Tier object
|
|
|
|
* @property {number} members Paid members on this tier
|
|
|
|
*/
|
|
|
|
|
2022-03-24 14:04:18 +03:00
|
|
|
/**
|
|
|
|
* @typedef SiteStatus Contains information on what graphs need to be shown
|
|
|
|
* @type {Object}
|
|
|
|
* @property {boolean} hasPaidTiers Whether the site has paid tiers
|
|
|
|
* @property {boolean} stripeEnabled Whether the site has stripe enabled
|
|
|
|
* @property {boolean} newslettersEnabled Whether the site has newsletters
|
|
|
|
* @property {boolean} membersEnabled Whether the site has members enabled
|
|
|
|
*/
|
|
|
|
|
2022-03-23 11:51:53 +03:00
|
|
|
export default class DashboardStatsService extends Service {
|
2022-03-23 13:53:32 +03:00
|
|
|
@service dashboardMocks;
|
2022-03-23 11:51:53 +03:00
|
|
|
|
2022-03-24 14:04:18 +03:00
|
|
|
/**
|
|
|
|
* @type {?SiteStatus} Contains information on what graphs need to be shown
|
|
|
|
*/
|
|
|
|
@tracked siteStatus = null;
|
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
/**
|
|
|
|
* @type {?MemberCounts} memberCounts
|
|
|
|
*/
|
2022-03-23 11:51:53 +03:00
|
|
|
@tracked
|
|
|
|
memberCounts = null;
|
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
/**
|
|
|
|
* @type {?MemberCountStat[]}
|
|
|
|
*/
|
|
|
|
@tracked
|
|
|
|
memberCountStats = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {?MrrStat[]}
|
|
|
|
*/
|
2022-03-23 11:51:53 +03:00
|
|
|
@tracked
|
2022-03-23 18:38:16 +03:00
|
|
|
mrrStats = null;
|
2022-03-23 11:51:53 +03:00
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
/**
|
|
|
|
* @type {PaidMembersByCadence} Number of members for annual and monthly plans
|
|
|
|
*/
|
2022-03-23 11:51:53 +03:00
|
|
|
@tracked
|
2022-03-23 18:38:16 +03:00
|
|
|
paidMembersByCadence = null;
|
2022-03-23 11:51:53 +03:00
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
/**
|
|
|
|
* @type {PaidMembersForTier[]} Number of members for each tier
|
|
|
|
*/
|
|
|
|
@tracked
|
|
|
|
paidMembersByTier = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {?number} Number of members last seen in last 30 days (could differ if filtered by member status)
|
|
|
|
*/
|
2022-03-23 11:51:53 +03:00
|
|
|
@tracked
|
|
|
|
membersLastSeen30d = null;
|
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
/**
|
|
|
|
* @type {?number} Number of members last seen in last 7 days (could differ if filtered by member status)
|
|
|
|
*/
|
2022-03-23 11:51:53 +03:00
|
|
|
@tracked
|
|
|
|
membersLastSeen7d = null;
|
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
/**
|
|
|
|
* @type {?MemberCounts} Number of members that are subscribed (grouped by status)
|
|
|
|
*/
|
2022-03-23 11:51:53 +03:00
|
|
|
@tracked
|
|
|
|
newsletterSubscribers = null;
|
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
/**
|
|
|
|
* @type {?number} Number of emails sent in last 30 days
|
|
|
|
*/
|
2022-03-23 11:51:53 +03:00
|
|
|
@tracked
|
|
|
|
emailsSent30d = null;
|
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
/**
|
|
|
|
* @type {?EmailOpenRateStat[]}
|
|
|
|
*/
|
2022-03-23 11:51:53 +03:00
|
|
|
@tracked
|
|
|
|
emailOpenRateStats = null;
|
|
|
|
|
2022-03-24 14:04:18 +03:00
|
|
|
/**
|
|
|
|
* Amount of days to load for member count and MRR related charts
|
|
|
|
*/
|
|
|
|
@tracked chartDays = 7;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Filter last seen by this status
|
|
|
|
* @type {'free'|'paid'|'total'}
|
|
|
|
*/
|
|
|
|
@tracked lastSeenFilterStatus = 'total';
|
|
|
|
|
|
|
|
loadSiteStatus() {
|
2022-03-24 17:32:34 +03:00
|
|
|
return this._loadSiteStatus.perform();
|
|
|
|
}
|
|
|
|
|
|
|
|
@task
|
|
|
|
*_loadSiteStatus() {
|
2022-03-24 14:04:18 +03:00
|
|
|
this.siteStatus = null;
|
|
|
|
if (this.dashboardMocks.enabled) {
|
2022-03-24 17:32:34 +03:00
|
|
|
yield this.dashboardMocks.loadSiteStatus();
|
|
|
|
this.siteStatus = {...this.dashboardMocks.siteStatus};
|
2022-03-24 14:04:18 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Normal implementation
|
|
|
|
// @todo
|
|
|
|
}
|
|
|
|
|
2022-03-23 11:51:53 +03:00
|
|
|
loadMembersCounts() {
|
2022-03-24 17:32:34 +03:00
|
|
|
return this._loadMembersCounts.perform();
|
|
|
|
}
|
|
|
|
|
|
|
|
@task
|
|
|
|
*_loadMembersCounts() {
|
|
|
|
this.memberCounts = null;
|
2022-03-23 13:53:32 +03:00
|
|
|
if (this.dashboardMocks.enabled) {
|
2022-03-24 17:32:34 +03:00
|
|
|
yield this.dashboardMocks.waitRandom();
|
2022-03-24 11:01:30 +03:00
|
|
|
if (this.dashboardMocks.memberCounts === null) {
|
|
|
|
return null;
|
|
|
|
}
|
2022-03-23 18:38:16 +03:00
|
|
|
this.memberCounts = {...this.dashboardMocks.memberCounts};
|
2022-03-23 11:51:53 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Normal implementation
|
|
|
|
// @todo
|
|
|
|
}
|
|
|
|
|
2022-03-24 17:32:34 +03:00
|
|
|
loadMemberCountStats() {
|
|
|
|
// todo: add proper logic to prevent duplicate calls + reuse results if nothing has changed
|
|
|
|
return this._loadMemberCountStats.perform();
|
|
|
|
}
|
|
|
|
|
2022-03-23 11:51:53 +03:00
|
|
|
/**
|
|
|
|
* Loads the members graphs
|
|
|
|
* - total paid
|
|
|
|
* - total members
|
2022-03-24 14:04:18 +03:00
|
|
|
* for each day in the last chartDays days
|
2022-03-23 11:51:53 +03:00
|
|
|
*/
|
2022-03-24 17:32:34 +03:00
|
|
|
@task
|
|
|
|
*_loadMemberCountStats() {
|
|
|
|
this.memberCountStats = null;
|
2022-03-23 13:53:32 +03:00
|
|
|
if (this.dashboardMocks.enabled) {
|
2022-03-24 17:32:34 +03:00
|
|
|
yield this.dashboardMocks.waitRandom();
|
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
if (this.dashboardMocks.memberCountStats === null) {
|
2022-03-24 17:32:34 +03:00
|
|
|
// Note: that this shouldn't happen
|
2022-03-23 18:38:16 +03:00
|
|
|
return null;
|
|
|
|
}
|
2022-03-24 14:04:18 +03:00
|
|
|
this.memberCountStats = this.fillMissingDates(this.dashboardMocks.memberCountStats.slice(-this.chartDays), {paid: 0, free: 0, comped: 0}, this.chartDays);
|
2022-03-23 11:51:53 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Normal implementation
|
|
|
|
// @todo
|
|
|
|
}
|
|
|
|
|
2022-03-24 17:32:34 +03:00
|
|
|
loadMrrStats() {
|
|
|
|
// todo: add proper logic to prevent duplicate calls + reuse results if nothing has changed
|
|
|
|
return this._loadMrrStats.perform();
|
|
|
|
}
|
|
|
|
|
2022-03-23 11:51:53 +03:00
|
|
|
/**
|
2022-03-24 14:04:18 +03:00
|
|
|
* Loads the mrr graphs for the current chartDays days
|
2022-03-23 11:51:53 +03:00
|
|
|
*/
|
2022-03-24 17:32:34 +03:00
|
|
|
@task
|
|
|
|
*_loadMrrStats() {
|
|
|
|
this.mrrStats = null;
|
2022-03-23 13:53:32 +03:00
|
|
|
if (this.dashboardMocks.enabled) {
|
2022-03-24 17:32:34 +03:00
|
|
|
yield this.dashboardMocks.waitRandom();
|
2022-03-23 18:38:16 +03:00
|
|
|
if (this.dashboardMocks.mrrStats === null) {
|
|
|
|
return null;
|
|
|
|
}
|
2022-03-24 14:04:18 +03:00
|
|
|
this.mrrStats = this.fillMissingDates(this.dashboardMocks.mrrStats.slice(-this.chartDays), {mrr: 0}, this.chartDays);
|
2022-03-23 11:51:53 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Normal implementation
|
|
|
|
// @todo
|
|
|
|
}
|
|
|
|
|
2022-03-24 17:32:34 +03:00
|
|
|
loadLastSeen() {
|
|
|
|
// todo: add proper logic to prevent duplicate calls + reuse results if nothing has changed
|
|
|
|
return this._loadLastSeen.perform();
|
|
|
|
}
|
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
/**
|
|
|
|
* Loads the mrr graphs
|
|
|
|
*/
|
2022-03-24 17:32:34 +03:00
|
|
|
@task
|
|
|
|
*_loadLastSeen() {
|
|
|
|
this.membersLastSeen30d = null;
|
|
|
|
this.membersLastSeen7d = null;
|
|
|
|
|
2022-03-23 13:53:32 +03:00
|
|
|
if (this.dashboardMocks.enabled) {
|
2022-03-24 17:32:34 +03:00
|
|
|
yield this.dashboardMocks.waitRandom();
|
2022-03-24 14:04:18 +03:00
|
|
|
if (this.lastSeenFilterStatus === 'paid') {
|
2022-03-23 18:38:16 +03:00
|
|
|
// @todo
|
|
|
|
}
|
2022-03-23 13:53:32 +03:00
|
|
|
this.membersLastSeen30d = this.dashboardMocks.membersLastSeen30d;
|
|
|
|
this.membersLastSeen7d = this.dashboardMocks.membersLastSeen7d;
|
2022-03-23 11:51:53 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Normal implementation
|
|
|
|
// @todo
|
|
|
|
}
|
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
loadPaidMembersByCadence() {
|
2022-03-24 17:32:34 +03:00
|
|
|
// todo: add proper logic to prevent duplicate calls + reuse results if nothing has changed
|
|
|
|
return this._loadPaidMembersByCadence.perform();
|
|
|
|
}
|
|
|
|
|
|
|
|
@task
|
|
|
|
*_loadPaidMembersByCadence() {
|
|
|
|
this.paidMembersByCadence = null;
|
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
if (this.dashboardMocks.enabled) {
|
2022-03-24 17:32:34 +03:00
|
|
|
yield this.dashboardMocks.waitRandom();
|
|
|
|
this.paidMembersByCadence = {...this.dashboardMocks.paidMembersByCadence};
|
2022-03-23 18:38:16 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Normal implementation
|
|
|
|
// @todo
|
|
|
|
}
|
|
|
|
|
|
|
|
loadPaidMembersByTier() {
|
2022-03-24 17:32:34 +03:00
|
|
|
// todo: add proper logic to prevent duplicate calls + reuse results if nothing has changed
|
|
|
|
return this._loadPaidMembersByTier.perform();
|
|
|
|
}
|
|
|
|
|
|
|
|
@task
|
|
|
|
*_loadPaidMembersByTier() {
|
|
|
|
this.paidMembersByTier = null;
|
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
if (this.dashboardMocks.enabled) {
|
2022-03-24 17:32:34 +03:00
|
|
|
yield this.dashboardMocks.waitRandom();
|
|
|
|
this.paidMembersByTier = this.dashboardMocks.paidMembersByTier.slice();
|
2022-03-23 18:38:16 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Normal implementation
|
|
|
|
// @todo
|
|
|
|
}
|
|
|
|
|
|
|
|
loadNewsletterSubscribers() {
|
2022-03-24 17:32:34 +03:00
|
|
|
// todo: add proper logic to prevent duplicate calls + reuse results if nothing has changed
|
|
|
|
return this._loadNewsletterSubscribers.perform();
|
|
|
|
}
|
|
|
|
|
|
|
|
@task
|
|
|
|
*_loadNewsletterSubscribers() {
|
|
|
|
this.newsletterSubscribers = null;
|
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
if (this.dashboardMocks.enabled) {
|
2022-03-24 17:32:34 +03:00
|
|
|
yield this.dashboardMocks.waitRandom();
|
2022-03-23 18:38:16 +03:00
|
|
|
this.newsletterSubscribers = this.dashboardMocks.newsletterSubscribers;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Normal implementation
|
|
|
|
// @todo
|
|
|
|
}
|
|
|
|
|
|
|
|
loadEmailsSent() {
|
2022-03-24 17:32:34 +03:00
|
|
|
// todo: add proper logic to prevent duplicate calls + reuse results if nothing has changed
|
|
|
|
return this._loadEmailsSent.perform();
|
|
|
|
}
|
|
|
|
|
|
|
|
@task
|
|
|
|
*_loadEmailsSent() {
|
|
|
|
this.emailsSent30d = null;
|
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
if (this.dashboardMocks.enabled) {
|
2022-03-24 17:32:34 +03:00
|
|
|
yield this.dashboardMocks.waitRandom();
|
2022-03-23 18:38:16 +03:00
|
|
|
this.emailsSent30d = this.dashboardMocks.emailsSent30d;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Normal implementation
|
|
|
|
// @todo
|
|
|
|
}
|
|
|
|
|
|
|
|
loadEmailOpenRateStats() {
|
2022-03-24 17:32:34 +03:00
|
|
|
// todo: add proper logic to prevent duplicate calls + reuse results if nothing has changed
|
|
|
|
return this._loadEmailOpenRateStats.perform();
|
|
|
|
}
|
|
|
|
|
|
|
|
@task
|
|
|
|
*_loadEmailOpenRateStats() {
|
|
|
|
this.emailOpenRateStats = null;
|
|
|
|
|
2022-03-23 18:38:16 +03:00
|
|
|
if (this.dashboardMocks.enabled) {
|
2022-03-24 17:32:34 +03:00
|
|
|
yield this.dashboardMocks.waitRandom();
|
2022-03-23 18:38:16 +03:00
|
|
|
this.emailOpenRateStats = this.dashboardMocks.emailOpenRateStats;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Normal implementation
|
|
|
|
// @todo
|
|
|
|
}
|
|
|
|
|
2022-03-23 13:53:32 +03:00
|
|
|
/**
|
|
|
|
* For now this is only used when reloading all the graphs after changing the mocked data
|
|
|
|
* @todo: reload only data that we loaded earlier
|
|
|
|
*/
|
2022-03-24 14:04:18 +03:00
|
|
|
reloadAll() {
|
2022-03-23 13:53:32 +03:00
|
|
|
this.loadMembersCounts();
|
2022-03-24 14:04:18 +03:00
|
|
|
this.loadMrrStats();
|
|
|
|
this.loadMemberCountStats();
|
|
|
|
this.loadLastSeen();
|
2022-03-23 18:38:16 +03:00
|
|
|
this.loadPaidMembersByCadence();
|
|
|
|
this.loadPaidMembersByTier();
|
|
|
|
|
|
|
|
this.loadNewsletterSubscribers();
|
|
|
|
this.loadEmailsSent();
|
|
|
|
this.loadEmailOpenRateStats();
|
2022-03-23 13:53:32 +03:00
|
|
|
}
|
2022-03-23 11:51:53 +03:00
|
|
|
|
2022-03-23 13:53:32 +03:00
|
|
|
/**
|
|
|
|
* Fill data to match a given amount of days
|
2022-03-23 18:38:16 +03:00
|
|
|
* @param {MemberCountStat[]|MrrStat[]} data
|
|
|
|
* @param {MemberCountStat|MrrStat} defaultData
|
|
|
|
* @param {number} days Amount of days to fill the graph with
|
2022-03-23 13:53:32 +03:00
|
|
|
*/
|
|
|
|
fillMissingDates(data, defaultData, days) {
|
|
|
|
let currentRangeDate = moment().subtract(days, 'days');
|
|
|
|
|
|
|
|
let endDate = moment().add(1, 'hour');
|
|
|
|
const output = [];
|
|
|
|
const firstDateInRangeIndex = data.findIndex((val) => {
|
|
|
|
return moment(val.date).isAfter(currentRangeDate);
|
|
|
|
});
|
|
|
|
let initialDateInRangeVal = firstDateInRangeIndex > 0 ? data[firstDateInRangeIndex - 1] : null;
|
|
|
|
if (firstDateInRangeIndex === 0 && !initialDateInRangeVal) {
|
|
|
|
initialDateInRangeVal = data[firstDateInRangeIndex];
|
|
|
|
}
|
|
|
|
if (data.length > 0 && !initialDateInRangeVal && firstDateInRangeIndex !== 0) {
|
|
|
|
initialDateInRangeVal = data[data.length - 1];
|
|
|
|
}
|
2022-03-23 11:51:53 +03:00
|
|
|
|
2022-03-23 13:53:32 +03:00
|
|
|
let lastVal = initialDateInRangeVal ? initialDateInRangeVal : defaultData;
|
2022-03-23 11:51:53 +03:00
|
|
|
|
2022-03-23 13:53:32 +03:00
|
|
|
while (currentRangeDate.isBefore(endDate)) {
|
|
|
|
let dateStr = currentRangeDate.format('YYYY-MM-DD');
|
|
|
|
const dataOnDate = data.find(d => d.date === dateStr);
|
2022-03-24 11:46:16 +03:00
|
|
|
lastVal = dataOnDate ? dataOnDate : {...lastVal, date: dateStr};
|
2022-03-23 13:53:32 +03:00
|
|
|
output.push(lastVal);
|
|
|
|
currentRangeDate = currentRangeDate.add(1, 'day');
|
|
|
|
}
|
|
|
|
return output;
|
|
|
|
}
|
2022-03-23 11:51:53 +03:00
|
|
|
}
|