2016-07-01 01:55:44 +08:00
|
|
|
import { ajax } from 'discourse/lib/ajax';
|
2015-06-23 01:46:51 +08:00
|
|
|
import round from "discourse/lib/round";
|
2018-01-13 10:43:49 +08:00
|
|
|
import { fillMissingDates } from 'discourse/lib/utilities';
|
2018-03-16 05:10:45 +08:00
|
|
|
import computed from 'ember-addons/ember-computed-decorators';
|
2015-06-23 01:46:51 +08:00
|
|
|
|
|
|
|
const Report = Discourse.Model.extend({
|
2018-05-03 21:41:41 +08:00
|
|
|
average: false,
|
|
|
|
|
2018-05-14 09:33:36 +08:00
|
|
|
@computed("type", "start_date", "end_date")
|
|
|
|
reportUrl(type, start_date, end_date) {
|
2018-05-14 14:31:50 +08:00
|
|
|
start_date = moment(start_date).locale('en').format("YYYY-MM-DD");
|
|
|
|
end_date = moment(end_date).locale('en').format("YYYY-MM-DD");
|
2018-05-14 09:33:36 +08:00
|
|
|
return Discourse.getURL(`/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}`);
|
|
|
|
},
|
2013-03-31 02:07:25 +08:00
|
|
|
|
2015-06-23 01:46:51 +08:00
|
|
|
valueAt(numDaysAgo) {
|
2013-03-31 02:07:25 +08:00
|
|
|
if (this.data) {
|
2018-05-14 14:31:50 +08:00
|
|
|
const wantedDate = moment().subtract(numDaysAgo, "days").locale('en').format("YYYY-MM-DD");
|
2015-08-05 00:23:56 +08:00
|
|
|
const item = this.data.find(d => d.x === wantedDate);
|
2013-03-31 02:07:25 +08:00
|
|
|
if (item) {
|
|
|
|
return item.y;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
},
|
|
|
|
|
2015-08-05 00:23:56 +08:00
|
|
|
valueFor(startDaysAgo, endDaysAgo) {
|
2013-03-31 02:07:25 +08:00
|
|
|
if (this.data) {
|
2015-08-05 00:23:56 +08:00
|
|
|
const earliestDate = moment().subtract(endDaysAgo, "days").startOf("day");
|
|
|
|
const latestDate = moment().subtract(startDaysAgo, "days").startOf("day");
|
|
|
|
var d, sum = 0, count = 0;
|
|
|
|
_.each(this.data, datum => {
|
2013-06-11 04:48:50 +08:00
|
|
|
d = moment(datum.x);
|
2015-08-05 00:23:56 +08:00
|
|
|
if (d >= earliestDate && d <= latestDate) {
|
2013-03-31 02:07:25 +08:00
|
|
|
sum += datum.y;
|
2015-08-05 00:23:56 +08:00
|
|
|
count++;
|
2013-03-31 02:07:25 +08:00
|
|
|
}
|
|
|
|
});
|
2015-08-10 16:06:33 +08:00
|
|
|
if (this.get("method") === "average" && count > 0) { sum /= count; }
|
2015-06-23 01:46:51 +08:00
|
|
|
return round(sum, -2);
|
2013-03-31 02:07:25 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-05-03 21:41:41 +08:00
|
|
|
todayCount: function() { return this.valueAt(0); }.property("data", "average"),
|
|
|
|
yesterdayCount: function() { return this.valueAt(1); }.property("data", "average"),
|
|
|
|
sevenDaysAgoCount: function() { return this.valueAt(7); }.property("data", "average"),
|
|
|
|
thirtyDaysAgoCount: function() { return this.valueAt(30); }.property("data", "average"),
|
|
|
|
lastSevenDaysCount: function() {
|
|
|
|
return this.averageCount(7, this.valueFor(1, 7));
|
|
|
|
}.property("data", "average"),
|
|
|
|
lastThirtyDaysCount: function() {
|
|
|
|
return this.averageCount(30, this.valueFor(1, 30));
|
|
|
|
}.property("data", "average"),
|
|
|
|
|
|
|
|
averageCount(count, value) {
|
|
|
|
return this.get("average") ? value / count : value;
|
|
|
|
},
|
2013-04-03 02:40:00 +08:00
|
|
|
|
2018-05-03 21:41:41 +08:00
|
|
|
@computed('yesterdayCount')
|
|
|
|
yesterdayTrend(yesterdayCount) {
|
|
|
|
const yesterdayVal = yesterdayCount;
|
2015-08-05 00:23:56 +08:00
|
|
|
const twoDaysAgoVal = this.valueAt(2);
|
2018-05-03 21:41:41 +08:00
|
|
|
const change = ((yesterdayVal - twoDaysAgoVal) / yesterdayVal) * 100;
|
|
|
|
|
|
|
|
if (change > 50) {
|
|
|
|
return "high-trending-up";
|
|
|
|
} else if (change > 0) {
|
2015-08-05 00:23:56 +08:00
|
|
|
return "trending-up";
|
2018-05-03 21:41:41 +08:00
|
|
|
} else if (change === 0) {
|
2015-08-05 00:23:56 +08:00
|
|
|
return "no-change";
|
2018-05-03 21:41:41 +08:00
|
|
|
} else if (change < -50) {
|
|
|
|
return "high-trending-down";
|
|
|
|
} else if (change < 0) {
|
|
|
|
return "trending-down";
|
2013-03-31 02:07:25 +08:00
|
|
|
}
|
2018-03-16 05:10:45 +08:00
|
|
|
},
|
2013-03-31 02:07:25 +08:00
|
|
|
|
2018-05-03 21:41:41 +08:00
|
|
|
@computed('lastSevenDaysCount')
|
|
|
|
sevenDayTrend(lastSevenDaysCount) {
|
|
|
|
const currentPeriod = lastSevenDaysCount;
|
2015-08-05 00:23:56 +08:00
|
|
|
const prevPeriod = this.valueFor(8, 14);
|
2018-04-26 20:49:41 +08:00
|
|
|
const change = ((currentPeriod - prevPeriod) / prevPeriod) * 100;
|
|
|
|
|
|
|
|
if (change > 50) {
|
|
|
|
return "high-trending-up";
|
|
|
|
} else if (change > 0) {
|
2015-08-05 00:23:56 +08:00
|
|
|
return "trending-up";
|
2018-04-26 20:49:41 +08:00
|
|
|
} else if (change === 0) {
|
2015-08-05 00:23:56 +08:00
|
|
|
return "no-change";
|
2018-04-26 20:49:41 +08:00
|
|
|
} else if (change < -50) {
|
|
|
|
return "high-trending-down";
|
|
|
|
} else if (change < 0) {
|
|
|
|
return "trending-down";
|
2013-03-31 02:07:25 +08:00
|
|
|
}
|
2018-03-16 05:10:45 +08:00
|
|
|
},
|
2013-03-31 02:07:25 +08:00
|
|
|
|
2018-05-11 11:30:21 +08:00
|
|
|
@computed('data')
|
|
|
|
currentTotal(data){
|
|
|
|
return _.reduce(data, (cur, pair) => cur + pair.y, 0);
|
|
|
|
},
|
|
|
|
|
|
|
|
@computed('data', 'currentTotal')
|
|
|
|
currentAverage(data, total) {
|
2018-05-14 09:12:52 +08:00
|
|
|
return data.length === 0 ? 0 : parseFloat((total / parseFloat(data.length)).toFixed(1));
|
2018-05-11 11:30:21 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
@computed('prev_period', 'currentTotal', 'currentAverage')
|
|
|
|
trend(prev, currentTotal, currentAverage) {
|
|
|
|
const total = this.get('average') ? currentAverage : currentTotal;
|
|
|
|
const change = ((total - prev) / total) * 100;
|
|
|
|
|
|
|
|
if (change > 50) {
|
|
|
|
return "high-trending-up";
|
|
|
|
} else if (change > 0) {
|
|
|
|
return "trending-up";
|
|
|
|
} else if (change === 0) {
|
|
|
|
return "no-change";
|
|
|
|
} else if (change < -50) {
|
|
|
|
return "high-trending-down";
|
|
|
|
} else if (change < 0) {
|
|
|
|
return "trending-down";
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-05-03 21:41:41 +08:00
|
|
|
@computed('prev30Days', 'lastThirtyDaysCount')
|
|
|
|
thirtyDayTrend(prev30Days, lastThirtyDaysCount) {
|
|
|
|
const currentPeriod = lastThirtyDaysCount;
|
|
|
|
const change = ((currentPeriod - prev30Days) / currentPeriod) * 100;
|
|
|
|
|
|
|
|
if (change > 50) {
|
|
|
|
return "high-trending-up";
|
|
|
|
} else if (change > 0) {
|
|
|
|
return "trending-up";
|
|
|
|
} else if (change === 0) {
|
|
|
|
return "no-change";
|
|
|
|
} else if (change < -50) {
|
|
|
|
return "high-trending-down";
|
|
|
|
} else if (change < 0) {
|
|
|
|
return "trending-down";
|
2013-03-31 02:07:25 +08:00
|
|
|
}
|
2018-03-16 05:10:45 +08:00
|
|
|
},
|
2013-04-17 06:37:35 +08:00
|
|
|
|
2018-03-16 05:10:45 +08:00
|
|
|
@computed('type')
|
|
|
|
icon(type) {
|
|
|
|
switch (type) {
|
2015-08-05 00:23:56 +08:00
|
|
|
case "flags": return "flag";
|
|
|
|
case "likes": return "heart";
|
2017-01-05 09:07:38 +08:00
|
|
|
case "bookmarks": return "bookmark";
|
|
|
|
default: return null;
|
2013-04-17 06:37:35 +08:00
|
|
|
}
|
2018-03-16 05:10:45 +08:00
|
|
|
},
|
2015-08-05 00:23:56 +08:00
|
|
|
|
2018-03-16 05:10:45 +08:00
|
|
|
@computed('type')
|
|
|
|
method(type) {
|
|
|
|
if (type === "time_to_first_response") {
|
2015-08-05 00:23:56 +08:00
|
|
|
return "average";
|
|
|
|
} else {
|
|
|
|
return "sum";
|
|
|
|
}
|
2018-03-16 05:10:45 +08:00
|
|
|
},
|
2013-04-27 05:13:20 +08:00
|
|
|
|
2015-06-23 01:46:51 +08:00
|
|
|
percentChangeString(val1, val2) {
|
2015-08-05 00:23:56 +08:00
|
|
|
const val = ((val1 - val2) / val2) * 100;
|
|
|
|
if (isNaN(val) || !isFinite(val)) {
|
2013-04-27 05:13:20 +08:00
|
|
|
return null;
|
2015-08-05 00:23:56 +08:00
|
|
|
} else if (val > 0) {
|
|
|
|
return "+" + val.toFixed(0) + "%";
|
2013-04-27 05:13:20 +08:00
|
|
|
} else {
|
2015-08-05 00:23:56 +08:00
|
|
|
return val.toFixed(0) + "%";
|
2013-04-27 05:13:20 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-05-11 11:30:21 +08:00
|
|
|
@computed('prev_period', 'currentTotal', 'currentAverage')
|
|
|
|
trendTitle(prev, currentTotal, currentAverage) {
|
|
|
|
let current = this.get('average') ? currentAverage : currentTotal;
|
|
|
|
let percent = this.percentChangeString(current, prev);
|
|
|
|
|
|
|
|
if (this.get('average')) {
|
2018-05-14 09:12:52 +08:00
|
|
|
prev = prev ? prev.toFixed(1) : "0";
|
2018-05-11 11:30:21 +08:00
|
|
|
current += '%';
|
|
|
|
prev += '%';
|
|
|
|
}
|
|
|
|
|
|
|
|
return I18n.t('admin.dashboard.reports.trend_title', {percent: percent, prev: prev, current: current});
|
|
|
|
},
|
|
|
|
|
2015-06-23 01:46:51 +08:00
|
|
|
changeTitle(val1, val2, prevPeriodString) {
|
2015-08-05 00:23:56 +08:00
|
|
|
const percentChange = this.percentChangeString(val1, val2);
|
|
|
|
var title = "";
|
|
|
|
if (percentChange) { title += percentChange + " change. "; }
|
|
|
|
title += "Was " + val2 + " " + prevPeriodString + ".";
|
2013-04-27 05:13:20 +08:00
|
|
|
return title;
|
|
|
|
},
|
|
|
|
|
2018-05-03 21:41:41 +08:00
|
|
|
@computed('yesterdayCount')
|
|
|
|
yesterdayCountTitle(yesterdayCount) {
|
|
|
|
return this.changeTitle(yesterdayCount, this.valueAt(2), "two days ago");
|
2018-03-16 05:10:45 +08:00
|
|
|
},
|
2013-04-27 05:13:20 +08:00
|
|
|
|
2018-05-03 21:41:41 +08:00
|
|
|
@computed('lastSevenDaysCount')
|
|
|
|
sevenDayCountTitle(lastSevenDaysCount) {
|
|
|
|
return this.changeTitle(lastSevenDaysCount, this.valueFor(8, 14), "two weeks ago");
|
2018-03-16 05:10:45 +08:00
|
|
|
},
|
2013-04-27 05:13:20 +08:00
|
|
|
|
2018-05-03 21:41:41 +08:00
|
|
|
@computed('prev30Days', 'lastThirtyDaysCount')
|
|
|
|
thirtyDayCountTitle(prev30Days, lastThirtyDaysCount) {
|
|
|
|
return this.changeTitle(lastThirtyDaysCount, prev30Days, "in the previous 30 day period");
|
2018-03-16 05:10:45 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
@computed('data')
|
|
|
|
sortedData(data) {
|
2018-03-28 01:53:47 +08:00
|
|
|
return this.get('xAxisIsDate') ? data.toArray().reverse() : data.toArray();
|
2018-03-16 05:10:45 +08:00
|
|
|
},
|
2014-06-07 05:08:35 +08:00
|
|
|
|
2018-03-16 05:10:45 +08:00
|
|
|
@computed('data')
|
2018-03-28 01:53:47 +08:00
|
|
|
xAxisIsDate() {
|
2018-03-16 05:10:45 +08:00
|
|
|
if (!this.data[0]) return false;
|
2018-03-28 02:10:39 +08:00
|
|
|
return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/);
|
2018-03-16 05:10:45 +08:00
|
|
|
}
|
2013-04-27 05:13:20 +08:00
|
|
|
|
2013-03-20 00:04:40 +08:00
|
|
|
});
|
2013-02-28 11:39:42 +08:00
|
|
|
|
2015-06-23 01:46:51 +08:00
|
|
|
Report.reopenClass({
|
2014-11-06 03:46:27 +08:00
|
|
|
|
2018-05-11 11:30:21 +08:00
|
|
|
fillMissingDates(report) {
|
2018-05-14 09:12:52 +08:00
|
|
|
if (_.isArray(report.data)) {
|
|
|
|
|
2018-05-14 14:31:50 +08:00
|
|
|
const startDateFormatted = moment.utc(report.start_date).locale('en').format('YYYY-MM-DD');
|
|
|
|
const endDateFormatted = moment.utc(report.end_date).locale('en').format('YYYY-MM-DD');
|
2018-05-11 11:30:21 +08:00
|
|
|
report.data = fillMissingDates(report.data, startDateFormatted, endDateFormatted);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-02-03 10:29:51 +08:00
|
|
|
find(type, startDate, endDate, categoryId, groupId) {
|
2016-07-01 01:55:44 +08:00
|
|
|
return ajax("/admin/reports/" + type, {
|
2015-06-24 21:19:39 +08:00
|
|
|
data: {
|
|
|
|
start_date: startDate,
|
|
|
|
end_date: endDate,
|
2016-02-03 10:29:51 +08:00
|
|
|
category_id: categoryId,
|
|
|
|
group_id: groupId
|
2015-06-24 21:19:39 +08:00
|
|
|
}
|
|
|
|
}).then(json => {
|
2018-01-13 10:43:49 +08:00
|
|
|
// Add zero values for missing dates
|
2018-05-11 14:25:41 +08:00
|
|
|
Report.fillMissingDates(json.report);
|
2018-01-13 10:43:49 +08:00
|
|
|
|
2015-12-02 07:31:30 +08:00
|
|
|
const model = Report.create({ type: type });
|
2014-07-22 01:39:23 +08:00
|
|
|
model.setProperties(json.report);
|
2018-03-16 05:10:45 +08:00
|
|
|
|
|
|
|
if (json.report.related_report) {
|
|
|
|
// TODO: fillMissingDates if xaxis is date
|
|
|
|
const related = Report.create({ type: json.report.related_report.type });
|
|
|
|
related.setProperties(json.report.related_report);
|
|
|
|
model.set('relatedReport', related);
|
|
|
|
}
|
|
|
|
|
2014-11-06 03:46:27 +08:00
|
|
|
return model;
|
2013-02-28 11:39:42 +08:00
|
|
|
});
|
|
|
|
}
|
2013-03-14 20:01:52 +08:00
|
|
|
});
|
2015-06-23 01:46:51 +08:00
|
|
|
|
|
|
|
export default Report;
|