Ghost/ghost/admin/app/services/members-stats.js
Kevin Ansfield fa84808048 Dropped ember-cli-moment-shim dependency
no issue

Since `ember-moment@10.0` it's not been necessary to use the `ember-cli-moment-shim` package, with `moment` instead being usable directly via `ember-auto-import`. Getting rid of the shim package is necessary for compatibility with `embroider`, Ember's new build tooling.

- dropped `ember-cli-moment-shim` dependency
- added `moment-timezone` dependency and updated all imports to reflect the different package
- worked around `ember-power-calendar` having `ember-cli-moment-shim` as a sub-dependency
  - added empty in-repo-addon `ember-power-calendar-moment` to avoid `ember-power-calendar` complaining about a missing package
  - added `ember-power-calendar-utils` in-repo-addon that is a copy of `ember-power-calendar-moment` but without the build-time renaming of the tree for better compatibility with embroider
2022-09-24 13:28:23 +02:00

260 lines
8.9 KiB
JavaScript

import Service, {inject as service} from '@ember/service';
import moment from 'moment-timezone';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
const ONE_MINUTE = 1 * 60 * 1000;
export default class MembersStatsService extends Service {
@service ajax;
@service ghostPaths;
@service store;
@tracked days = '30';
@tracked stats = null;
@tracked events = null;
@tracked countStats = null;
@tracked mrrStats = null;
@tracked newsletterStats = null;
@tracked totalMemberCount = null;
get memberCount() {
let stats = this.totalMemberCount;
if (!stats) {
return 0;
}
const {free, paid, comped} = stats.meta.totals;
const total = free + paid + comped || 0;
return total;
}
fetch() {
let daysChanged = this._lastFetchedDays !== this.days;
let staleData = this._lastFetched && (new Date() - this._lastFetched) > ONE_MINUTE;
// return an already in-progress promise unless params have changed
if (this._fetchTask.isRunning && !this._forceRefresh && !daysChanged) {
return this._fetchTask.last;
}
// return existing stats unless data is > 1 min old
if (this.stats && !this._forceRefresh && !daysChanged && !staleData) {
return Promise.resolve(this.stats);
}
return this._fetchTask.perform();
}
fetchCounts() {
let staleData = this._lastFetchedCounts && (new Date() - this._lastFetchedCounts) > ONE_MINUTE;
// return an already in-progress promise unless params have changed
if (this._fetchCountsTask.isRunning) {
return this._fetchCountsTask.last;
}
// return existing stats unless data is > 1 min old
if (this.countStats && !this._forceRefresh && !staleData) {
return Promise.resolve(this.countStats);
}
return this._fetchCountsTask.perform();
}
fetchMemberCount() {
let staleData = this._lastFetchedMemberCounts && (new Date() - this._lastFetchedMemberCounts) > ONE_MINUTE;
// return an already in-progress promise unless params have changed
if (this._fetchMemberCountsTask.isRunning) {
return this._fetchMemberCountsTask.last;
}
// return existing stats unless data is > 1 min old
if (this.totalMemberCount && !this._forceRefresh && !staleData) {
return Promise.resolve(this.totalMemberCount);
}
return this._fetchMemberCountsTask.perform();
}
fetchNewsletterStats() {
let staleData = this._lastFetchedNewsletterStats && (new Date() - this._lastFetchedNewsletterStats) > ONE_MINUTE;
// return an already in-progress promise unless params have changed
if (this._fetchNewsletterStatsTask.isRunning) {
return this._fetchNewsletterStatsTask.last;
}
// return existing stats unless data is > 1 min old
if (this.newsletterStats && !this._forceRefresh && !staleData) {
return Promise.resolve(this.newsletterStats);
}
return this._fetchNewsletterStatsTask.perform();
}
fillDates(data = []) {
let currentRangeDate = moment().subtract(30, '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];
}
let lastVal = initialDateInRangeVal ? initialDateInRangeVal.value : 0;
while (currentRangeDate.isBefore(endDate)) {
let dateStr = currentRangeDate.format('YYYY-MM-DD');
const dataOnDate = data.find(d => d.date === dateStr);
output[dateStr] = dataOnDate ? dataOnDate.value : lastVal;
lastVal = output[dateStr];
currentRangeDate = currentRangeDate.add(1, 'day');
}
return output;
}
fillCountDates(data = {}) {
let currentRangeDate = moment().subtract(30, '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];
}
let lastVal = {
paid: initialDateInRangeVal ? initialDateInRangeVal.paid : 0,
free: initialDateInRangeVal ? initialDateInRangeVal.free : 0,
comped: initialDateInRangeVal ? initialDateInRangeVal.comped : 0,
total: initialDateInRangeVal ? (initialDateInRangeVal.paid + initialDateInRangeVal.free + initialDateInRangeVal.comped) : 0
};
while (currentRangeDate.isBefore(endDate)) {
let dateStr = currentRangeDate.format('YYYY-MM-DD');
const dataOnDate = data.find(d => d.date === dateStr);
output[dateStr] = dataOnDate ? {
paid: dataOnDate.paid,
free: dataOnDate.free,
comped: dataOnDate.comped,
total: dataOnDate.paid + dataOnDate.free + dataOnDate.comped
} : lastVal;
lastVal = output[dateStr];
currentRangeDate = currentRangeDate.add(1, 'day');
}
return output;
}
fetchMRR() {
let staleData = this._lastFetchedMRR && (new Date() - this._lastFetchedMRR) > ONE_MINUTE;
// return an already in-progress promise unless params have changed
if (this._fetchMRRTask.isRunning) {
return this._fetchMRRTask.last;
}
// return existing stats unless data is > 1 min old
if (this.mrrStats && !this._forceRefresh && !staleData) {
return Promise.resolve(this.mrrStats);
}
return this._fetchMRRTask.perform();
}
invalidate() {
this._forceRefresh = true;
}
@task
*_fetchNewsletterStatsTask() {
const limit = 5;
let query = {
filter: 'email_count:-0',
order: 'submitted_at desc',
limit: limit
};
const results = yield this.store.query('email', query);
const data = results.toArray();
let stats = data.map((d) => {
return {
subject: d.subject,
submittedAt: moment(d.submittedAtUTC).format('YYYY-MM-DD'),
openRate: d.openRate
};
});
const paddedResults = [];
if (data.length < limit) {
const pad = limit - data.length;
const lastSubmittedAt = data.length > 0 ? data[results.length - 1].submittedAtUTC : moment();
for (let i = 0; i < pad; i++) {
paddedResults.push({
subject: '',
submittedAt: moment(lastSubmittedAt).subtract(i + 1, 'days').format('YYYY-MM-DD'),
openRate: 0
});
}
}
stats = stats .concat(paddedResults);
stats.reverse();
this.newsletterStats = stats;
return stats;
}
@task
*_fetchCountsTask() {
this._lastFetchedCounts = new Date();
let statsUrl = this.ghostPaths.url.api('members/stats/count');
let stats = yield this.ajax.request(statsUrl);
this.countStats = stats;
return stats;
}
@task
*_fetchMemberCountsTask() {
this._lastFetchedMemberCounts = new Date();
let statsUrl = this.ghostPaths.url.api('stats/member_count/');
let stats = yield this.ajax.request(statsUrl);
this.totalMemberCount = stats;
return stats;
}
@task
*_fetchMRRTask() {
this._lastFetchedMRR = new Date();
let statsUrl = this.ghostPaths.url.api('members/stats/mrr');
let stats = yield this.ajax.request(statsUrl);
this.mrrStats = stats;
return stats;
}
@task
*_fetchTask() {
let {days} = this;
this._lastFetchedDays = days;
this._lastFetched = new Date();
this._forceRefresh = false;
let statsUrl = this.ghostPaths.url.api('members/stats');
let stats = yield this.ajax.request(statsUrl, {data: {days}});
this.stats = stats;
return stats;
}
}