2021-02-04 20:36:29 +03:00
|
|
|
import Controller from '@ember/controller';
|
2021-02-25 12:57:46 +03:00
|
|
|
import {action} from '@ember/object';
|
2021-02-19 08:48:01 +03:00
|
|
|
import {getSymbol} from 'ghost-admin/utils/currency';
|
2021-02-04 20:36:29 +03:00
|
|
|
import {inject as service} from '@ember/service';
|
2021-02-18 17:17:10 +03:00
|
|
|
import {tracked} from '@glimmer/tracking';
|
2021-02-04 20:36:29 +03:00
|
|
|
|
|
|
|
export default class DashboardController extends Controller {
|
2021-02-04 21:35:19 +03:00
|
|
|
@service feature;
|
2021-02-04 20:36:29 +03:00
|
|
|
@service session;
|
2021-02-18 17:17:10 +03:00
|
|
|
@service membersStats;
|
2021-02-19 15:12:53 +03:00
|
|
|
@service store;
|
2021-02-19 16:55:35 +03:00
|
|
|
@service settings;
|
2021-02-24 18:59:01 +03:00
|
|
|
@service whatsNew;
|
2021-02-18 17:17:10 +03:00
|
|
|
|
2021-04-20 11:01:04 +03:00
|
|
|
@tracked eventsData = null;
|
|
|
|
@tracked eventsError = null;
|
|
|
|
@tracked eventsLoading = false;
|
|
|
|
|
|
|
|
@tracked mrrStatsData = null;
|
|
|
|
@tracked mrrStatsError = null;
|
|
|
|
@tracked mrrStatsLoading = false;
|
|
|
|
|
|
|
|
@tracked memberCountStatsData = null;
|
|
|
|
@tracked memberCountStatsError = null;
|
|
|
|
@tracked memberCountStatsLoading = false;
|
|
|
|
|
|
|
|
@tracked topMembersData = null;
|
|
|
|
@tracked topMembersError = null;
|
|
|
|
@tracked topMembersLoading = false;
|
|
|
|
|
|
|
|
@tracked newsletterOpenRatesData = null;
|
|
|
|
@tracked newsletterOpenRatesError = null;
|
|
|
|
@tracked newsletterOpenRatesLoading = false;
|
|
|
|
|
|
|
|
@tracked whatsNewEntries = null;
|
|
|
|
@tracked whatsNewEntriesLoading = null;
|
|
|
|
@tracked whatsNewEntriesError = null;
|
2021-02-24 18:59:01 +03:00
|
|
|
|
2021-03-02 15:08:07 +03:00
|
|
|
get topMembersDataHasOpenRates() {
|
|
|
|
return this.topMembersData && this.topMembersData.find((member) => {
|
|
|
|
return member.emailOpenRate !== null;
|
|
|
|
});
|
2021-02-19 15:12:53 +03:00
|
|
|
}
|
|
|
|
|
2021-04-20 11:08:15 +03:00
|
|
|
get showMembersData() {
|
|
|
|
return this.settings.get('membersSignupAccess') !== 'none';
|
|
|
|
}
|
|
|
|
|
2021-02-23 17:34:13 +03:00
|
|
|
initialise() {
|
2021-02-18 17:17:10 +03:00
|
|
|
this.loadEvents();
|
2021-02-19 15:12:53 +03:00
|
|
|
this.loadTopMembers();
|
2021-02-18 20:13:51 +03:00
|
|
|
this.loadCharts();
|
2021-02-24 18:59:01 +03:00
|
|
|
this.loadWhatsNew();
|
2021-02-18 20:13:51 +03:00
|
|
|
}
|
|
|
|
|
2021-06-24 12:16:44 +03:00
|
|
|
async loadMRRStats() {
|
|
|
|
const products = await this.store.query('product', {include: 'monthly_price,yearly_price', limit: 'all'});
|
|
|
|
const defaultProduct = products?.firstObject;
|
|
|
|
|
2021-02-25 11:34:44 +03:00
|
|
|
this.mrrStatsLoading = true;
|
2021-02-18 20:13:51 +03:00
|
|
|
this.membersStats.fetchMRR().then((stats) => {
|
2021-02-19 20:17:28 +03:00
|
|
|
this.mrrStatsLoading = false;
|
2021-03-24 18:46:02 +03:00
|
|
|
const statsData = stats.data || [];
|
2021-06-24 12:16:44 +03:00
|
|
|
const defaultCurrency = defaultProduct?.monthlyPrice?.currency || 'usd';
|
2021-03-24 18:46:02 +03:00
|
|
|
let currencyStats = statsData.find((stat) => {
|
|
|
|
return stat.currency === defaultCurrency;
|
|
|
|
});
|
|
|
|
currencyStats = currencyStats || {
|
2021-02-19 15:50:41 +03:00
|
|
|
data: [],
|
2021-03-24 18:46:02 +03:00
|
|
|
currency: defaultCurrency
|
2021-02-19 15:50:41 +03:00
|
|
|
};
|
2021-02-19 08:48:01 +03:00
|
|
|
if (currencyStats) {
|
2021-02-25 18:32:50 +03:00
|
|
|
const currencyStatsData = this.membersStats.fillDates(currencyStats.data) || {};
|
2021-03-24 18:56:16 +03:00
|
|
|
const dateValues = Object.values(currencyStatsData).map(val => Math.round((val / 100)));
|
2021-02-19 08:48:01 +03:00
|
|
|
const currentMRR = dateValues.length ? dateValues[dateValues.length - 1] : 0;
|
2021-02-19 15:50:41 +03:00
|
|
|
const rangeStartMRR = dateValues.length ? dateValues[0] : 0;
|
2021-02-22 12:17:05 +03:00
|
|
|
const percentGrowth = rangeStartMRR !== 0 ? ((currentMRR - rangeStartMRR) / rangeStartMRR) * 100 : 0;
|
2021-02-19 20:17:28 +03:00
|
|
|
this.mrrStatsData = {
|
2021-03-08 20:08:53 +03:00
|
|
|
currentAmount: currentMRR,
|
|
|
|
currency: getSymbol(currencyStats.currency),
|
2021-02-22 12:17:05 +03:00
|
|
|
percentGrowth: percentGrowth.toFixed(1),
|
2021-02-24 12:32:10 +03:00
|
|
|
percentClass: (percentGrowth > 0 ? 'positive' : (percentGrowth < 0 ? 'negative' : '')),
|
2021-02-19 08:48:01 +03:00
|
|
|
options: {
|
|
|
|
rangeInDays: 30
|
|
|
|
},
|
|
|
|
data: {
|
|
|
|
label: 'MRR',
|
2021-02-25 18:32:50 +03:00
|
|
|
dateLabels: Object.keys(currencyStatsData),
|
2021-02-19 08:48:01 +03:00
|
|
|
dateValues
|
|
|
|
},
|
|
|
|
title: 'MRR',
|
|
|
|
stats: currencyStats
|
|
|
|
};
|
|
|
|
}
|
2021-02-18 20:13:51 +03:00
|
|
|
}, (error) => {
|
2021-02-19 20:17:28 +03:00
|
|
|
this.mrrStatsError = error;
|
|
|
|
this.mrrStatsLoading = false;
|
2021-02-18 20:13:51 +03:00
|
|
|
});
|
2021-02-18 17:17:10 +03:00
|
|
|
}
|
|
|
|
|
2021-02-19 08:48:01 +03:00
|
|
|
loadMemberCountStats() {
|
2021-02-25 11:34:44 +03:00
|
|
|
this.memberCountStatsLoading = true;
|
2021-02-19 08:48:01 +03:00
|
|
|
this.membersStats.fetchCounts().then((stats) => {
|
2021-02-19 20:17:28 +03:00
|
|
|
this.memberCountStatsLoading = false;
|
2021-02-19 08:48:01 +03:00
|
|
|
|
|
|
|
if (stats) {
|
2021-02-25 18:32:50 +03:00
|
|
|
const statsDateObj = this.membersStats.fillCountDates(stats.data) || {};
|
|
|
|
const dateValues = Object.values(statsDateObj);
|
2021-02-22 12:17:05 +03:00
|
|
|
const currentAllCount = dateValues.length ? dateValues[dateValues.length - 1].total : 0;
|
|
|
|
const currentPaidCount = dateValues.length ? dateValues[dateValues.length - 1].paid : 0;
|
|
|
|
const rangeStartAllCount = dateValues.length ? dateValues[0].total : 0;
|
|
|
|
const rangeStartPaidCount = dateValues.length ? dateValues[0].paid : 0;
|
|
|
|
const allCountPercentGrowth = rangeStartAllCount !== 0 ? ((currentAllCount - rangeStartAllCount) / rangeStartAllCount) * 100 : 0;
|
|
|
|
const paidCountPercentGrowth = rangeStartPaidCount !== 0 ? ((currentPaidCount - rangeStartPaidCount) / rangeStartPaidCount) * 100 : 0;
|
2021-02-19 08:48:01 +03:00
|
|
|
|
2021-02-19 20:17:28 +03:00
|
|
|
this.memberCountStatsData = {
|
2021-02-19 08:48:01 +03:00
|
|
|
all: {
|
2021-02-22 12:17:05 +03:00
|
|
|
percentGrowth: allCountPercentGrowth.toFixed(1),
|
2021-02-24 12:32:10 +03:00
|
|
|
percentClass: (allCountPercentGrowth > 0 ? 'positive' : (allCountPercentGrowth < 0 ? 'negative' : '')),
|
2021-02-19 08:48:01 +03:00
|
|
|
total: dateValues.length ? dateValues[dateValues.length - 1].total : 0,
|
|
|
|
options: {
|
|
|
|
rangeInDays: 30
|
|
|
|
},
|
|
|
|
data: {
|
|
|
|
label: 'Members',
|
2021-02-25 18:32:50 +03:00
|
|
|
dateLabels: Object.keys(statsDateObj),
|
2021-02-19 08:48:01 +03:00
|
|
|
dateValues: dateValues.map(d => d.total)
|
|
|
|
},
|
|
|
|
title: 'Total Members',
|
|
|
|
stats: stats
|
|
|
|
},
|
|
|
|
paid: {
|
2021-02-22 12:17:05 +03:00
|
|
|
percentGrowth: paidCountPercentGrowth.toFixed(1),
|
2021-02-24 12:32:10 +03:00
|
|
|
percentClass: (paidCountPercentGrowth > 0 ? 'positive' : (paidCountPercentGrowth < 0 ? 'negative' : '')),
|
2021-02-19 08:48:01 +03:00
|
|
|
total: dateValues.length ? dateValues[dateValues.length - 1].paid : 0,
|
|
|
|
options: {
|
|
|
|
rangeInDays: 30
|
|
|
|
},
|
|
|
|
data: {
|
|
|
|
label: 'Members',
|
2021-02-25 18:32:50 +03:00
|
|
|
dateLabels: Object.keys(statsDateObj),
|
2021-02-19 08:48:01 +03:00
|
|
|
dateValues: dateValues.map(d => d.paid)
|
|
|
|
},
|
|
|
|
title: 'Paid Members',
|
|
|
|
stats: stats
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}, (error) => {
|
2021-02-19 20:17:28 +03:00
|
|
|
this.memberCountStatsError = error;
|
|
|
|
this.memberCountStatsLoading = false;
|
2021-02-19 08:48:01 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
loadCharts() {
|
|
|
|
this.loadMRRStats();
|
|
|
|
this.loadMemberCountStats();
|
2021-02-22 11:29:48 +03:00
|
|
|
this.loadNewsletterOpenRates();
|
2021-02-19 08:48:01 +03:00
|
|
|
}
|
|
|
|
|
2021-02-18 17:17:10 +03:00
|
|
|
loadEvents() {
|
2021-02-19 20:17:28 +03:00
|
|
|
this.eventsLoading = true;
|
2021-02-23 15:46:17 +03:00
|
|
|
this.membersStats.fetchTimeline({limit: 5}).then(({events}) => {
|
2021-02-19 20:17:28 +03:00
|
|
|
this.eventsData = events;
|
|
|
|
this.eventsLoading = false;
|
2021-02-18 17:17:10 +03:00
|
|
|
}, (error) => {
|
2021-02-19 20:17:28 +03:00
|
|
|
this.eventsError = error;
|
|
|
|
this.eventsLoading = false;
|
2021-02-18 17:17:10 +03:00
|
|
|
});
|
|
|
|
}
|
2021-02-19 15:12:53 +03:00
|
|
|
|
2021-02-22 11:29:48 +03:00
|
|
|
loadNewsletterOpenRates() {
|
|
|
|
this.newsletterOpenRatesLoading = true;
|
|
|
|
this.membersStats.fetchNewsletterStats().then((results) => {
|
2021-02-25 11:04:34 +03:00
|
|
|
const rangeStartOpenRate = results.length > 1 ? results[results.length - 2].openRate : 0;
|
|
|
|
const rangeEndOpenRate = results.length > 0 ? results[results.length - 1].openRate : 0;
|
|
|
|
const percentGrowth = rangeStartOpenRate !== 0 ? ((rangeEndOpenRate - rangeStartOpenRate) / rangeStartOpenRate) * 100 : 0;
|
2021-02-22 11:29:48 +03:00
|
|
|
this.newsletterOpenRatesData = {
|
2021-02-25 11:04:34 +03:00
|
|
|
percentGrowth: percentGrowth.toFixed(1),
|
2021-03-05 14:46:32 +03:00
|
|
|
percentClass: (percentGrowth > 0 ? 'positive' : (percentGrowth < 0 ? 'negative' : '')),
|
2021-02-25 11:04:34 +03:00
|
|
|
current: rangeEndOpenRate,
|
2021-02-22 11:29:48 +03:00
|
|
|
options: {
|
|
|
|
rangeInDays: 30
|
|
|
|
},
|
|
|
|
data: {
|
2021-02-23 14:50:54 +03:00
|
|
|
label: 'Open rate',
|
|
|
|
dateLabels: results.map(d => d.subject),
|
2021-02-22 11:29:48 +03:00
|
|
|
dateValues: results.map(d => d.openRate)
|
|
|
|
},
|
2021-02-23 14:50:54 +03:00
|
|
|
title: 'Open rate',
|
2021-02-22 11:29:48 +03:00
|
|
|
stats: results
|
|
|
|
};
|
|
|
|
this.newsletterOpenRatesLoading = false;
|
|
|
|
}, (error) => {
|
|
|
|
this.newsletterOpenRatesError = error;
|
|
|
|
this.newsletterOpenRatesLoading = false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-02-19 15:12:53 +03:00
|
|
|
loadTopMembers() {
|
2021-02-19 20:17:28 +03:00
|
|
|
this.topMembersLoading = true;
|
2021-02-19 15:12:53 +03:00
|
|
|
let query = {
|
|
|
|
filter: 'email_open_rate:-null',
|
|
|
|
order: 'email_open_rate desc',
|
2021-03-05 14:43:54 +03:00
|
|
|
limit: 5
|
2021-02-19 15:12:53 +03:00
|
|
|
};
|
|
|
|
this.store.query('member', query).then((result) => {
|
2021-03-02 15:08:07 +03:00
|
|
|
if (!result.length) {
|
|
|
|
return this.store.query('member', {
|
|
|
|
filter: 'status:paid',
|
|
|
|
order: 'created_at asc',
|
2021-03-05 14:43:54 +03:00
|
|
|
limit: 5
|
2021-03-02 15:08:07 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}).then((result) => {
|
2021-02-19 20:17:28 +03:00
|
|
|
this.topMembersData = result;
|
|
|
|
this.topMembersLoading = false;
|
2021-03-02 15:08:07 +03:00
|
|
|
}).catch((error) => {
|
2021-02-19 20:17:28 +03:00
|
|
|
this.topMembersError = error;
|
|
|
|
this.topMembersLoading = false;
|
2021-02-19 15:12:53 +03:00
|
|
|
});
|
|
|
|
}
|
2021-02-24 18:59:01 +03:00
|
|
|
|
|
|
|
loadWhatsNew() {
|
2021-02-25 11:51:28 +03:00
|
|
|
this.whatsNewEntriesLoading = true;
|
2021-02-24 18:59:01 +03:00
|
|
|
this.whatsNew.fetchLatest.perform().then(() => {
|
2021-02-25 11:51:28 +03:00
|
|
|
this.whatsNewEntriesLoading = false;
|
2021-02-24 18:59:01 +03:00
|
|
|
this.whatsNewEntries = this.whatsNew.entries.slice(0, 3);
|
2021-02-25 11:51:28 +03:00
|
|
|
}, (error) => {
|
|
|
|
this.whatsNewEntriesError = error;
|
|
|
|
this.whatsNewEntriesLoading = false;
|
2021-02-24 18:59:01 +03:00
|
|
|
});
|
|
|
|
}
|
2021-02-25 12:57:46 +03:00
|
|
|
|
2021-02-25 18:32:50 +03:00
|
|
|
@action
|
2021-02-25 12:57:46 +03:00
|
|
|
dismissLaunchBanner() {
|
2021-11-04 15:34:39 +03:00
|
|
|
this.settings.set('editorIsLaunchComplete', true);
|
|
|
|
this.settings.save();
|
2021-02-25 12:57:46 +03:00
|
|
|
}
|
2021-02-18 17:17:10 +03:00
|
|
|
}
|