2018-07-20 02:33:11 +08:00
|
|
|
|
import { escapeExpression } from "discourse/lib/utilities";
|
2018-05-18 04:44:33 +08:00
|
|
|
|
import { ajax } from "discourse/lib/ajax";
|
2015-06-23 01:46:51 +08:00
|
|
|
|
import round from "discourse/lib/round";
|
2018-08-01 05:35:13 +08:00
|
|
|
|
import {
|
|
|
|
|
fillMissingDates,
|
|
|
|
|
isNumeric,
|
|
|
|
|
formatUsername
|
|
|
|
|
} from "discourse/lib/utilities";
|
2018-05-18 04:44:33 +08:00
|
|
|
|
import computed from "ember-addons/ember-computed-decorators";
|
2018-07-20 02:33:11 +08:00
|
|
|
|
import { number, durationTiny } from "discourse/lib/formatter";
|
2018-08-01 05:35:13 +08:00
|
|
|
|
import { renderAvatar } from "discourse/helpers/user-avatar";
|
|
|
|
|
|
|
|
|
|
// Change this line each time report format change
|
|
|
|
|
// and you want to ensure cache is reset
|
|
|
|
|
export const SCHEMA_VERSION = 1;
|
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-16 02:12:03 +08:00
|
|
|
|
percent: false,
|
2018-05-18 04:44:33 +08:00
|
|
|
|
higher_is_better: true,
|
2018-05-03 21:41:41 +08:00
|
|
|
|
|
2018-07-20 02:33:11 +08:00
|
|
|
|
@computed("modes")
|
|
|
|
|
onlyTable(modes) {
|
|
|
|
|
return modes.length === 1 && modes[0] === "table";
|
|
|
|
|
},
|
|
|
|
|
|
2018-05-14 09:33:36 +08:00
|
|
|
|
@computed("type", "start_date", "end_date")
|
|
|
|
|
reportUrl(type, start_date, end_date) {
|
2018-07-20 02:33:11 +08:00
|
|
|
|
start_date = moment
|
|
|
|
|
.utc(start_date)
|
2018-06-15 23:03:24 +08:00
|
|
|
|
.locale("en")
|
|
|
|
|
.format("YYYY-MM-DD");
|
2018-07-20 02:33:11 +08:00
|
|
|
|
|
|
|
|
|
end_date = moment
|
|
|
|
|
.utc(end_date)
|
2018-06-15 23:03:24 +08:00
|
|
|
|
.locale("en")
|
|
|
|
|
.format("YYYY-MM-DD");
|
2018-07-20 02:33:11 +08:00
|
|
|
|
|
2018-06-15 23:03:24 +08:00
|
|
|
|
return Discourse.getURL(
|
|
|
|
|
`/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}`
|
|
|
|
|
);
|
2018-05-14 09:33:36 +08:00
|
|
|
|
},
|
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-06-15 23:03:24 +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) {
|
2018-06-15 23:03:24 +08:00
|
|
|
|
const earliestDate = moment()
|
|
|
|
|
.subtract(endDaysAgo, "days")
|
|
|
|
|
.startOf("day");
|
|
|
|
|
const latestDate = moment()
|
|
|
|
|
.subtract(startDaysAgo, "days")
|
|
|
|
|
.startOf("day");
|
|
|
|
|
let d,
|
|
|
|
|
sum = 0,
|
|
|
|
|
count = 0;
|
2015-08-05 00:23:56 +08:00
|
|
|
|
_.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
|
|
|
|
}
|
|
|
|
|
});
|
2018-06-15 23:03:24 +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-06-15 23:03:24 +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"),
|
2018-05-18 04:44:33 +08:00
|
|
|
|
|
2018-06-15 23:03:24 +08:00
|
|
|
|
lastSevenDaysCount: function() {
|
2018-05-03 21:41:41 +08:00
|
|
|
|
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-28 16:55:42 +08:00
|
|
|
|
@computed("yesterdayCount", "higher_is_better")
|
|
|
|
|
yesterdayTrend(yesterdayCount, higherIsBetter) {
|
|
|
|
|
return this._computeTrend(this.valueAt(2), yesterdayCount, higherIsBetter);
|
2018-03-16 05:10:45 +08:00
|
|
|
|
},
|
2013-03-31 02:07:25 +08:00
|
|
|
|
|
2018-05-28 16:55:42 +08:00
|
|
|
|
@computed("lastSevenDaysCount", "higher_is_better")
|
|
|
|
|
sevenDaysTrend(lastSevenDaysCount, higherIsBetter) {
|
2018-06-15 23:03:24 +08:00
|
|
|
|
return this._computeTrend(
|
|
|
|
|
this.valueFor(8, 14),
|
|
|
|
|
lastSevenDaysCount,
|
|
|
|
|
higherIsBetter
|
|
|
|
|
);
|
2018-03-16 05:10:45 +08:00
|
|
|
|
},
|
2013-03-31 02:07:25 +08:00
|
|
|
|
|
2018-05-18 04:44:33 +08:00
|
|
|
|
@computed("data")
|
2018-06-15 23:03:24 +08:00
|
|
|
|
currentTotal(data) {
|
2018-05-11 11:30:21 +08:00
|
|
|
|
return _.reduce(data, (cur, pair) => cur + pair.y, 0);
|
|
|
|
|
},
|
|
|
|
|
|
2018-05-18 04:44:33 +08:00
|
|
|
|
@computed("data", "currentTotal")
|
2018-05-11 11:30:21 +08:00
|
|
|
|
currentAverage(data, total) {
|
2018-06-15 23:03:24 +08:00
|
|
|
|
return Ember.makeArray(data).length === 0
|
|
|
|
|
? 0
|
|
|
|
|
: parseFloat((total / parseFloat(data.length)).toFixed(1));
|
2018-05-16 02:12:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
2018-05-25 18:09:30 +08:00
|
|
|
|
@computed("trend", "higher_is_better")
|
|
|
|
|
trendIcon(trend, higherIsBetter) {
|
|
|
|
|
return this._iconForTrend(trend, higherIsBetter);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
@computed("sevenDaysTrend", "higher_is_better")
|
|
|
|
|
sevenDaysTrendIcon(sevenDaysTrend, higherIsBetter) {
|
|
|
|
|
return this._iconForTrend(sevenDaysTrend, higherIsBetter);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
@computed("thirtyDaysTrend", "higher_is_better")
|
|
|
|
|
thirtyDaysTrendIcon(thirtyDaysTrend, higherIsBetter) {
|
|
|
|
|
return this._iconForTrend(thirtyDaysTrend, higherIsBetter);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
@computed("yesterdayTrend", "higher_is_better")
|
|
|
|
|
yesterdayTrendIcon(yesterdayTrend, higherIsBetter) {
|
|
|
|
|
return this._iconForTrend(yesterdayTrend, higherIsBetter);
|
2018-05-11 11:30:21 +08:00
|
|
|
|
},
|
|
|
|
|
|
2018-05-28 16:55:42 +08:00
|
|
|
|
@computed("prev_period", "currentTotal", "currentAverage", "higher_is_better")
|
|
|
|
|
trend(prev, currentTotal, currentAverage, higherIsBetter) {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
const total = this.get("average") ? currentAverage : currentTotal;
|
2018-05-28 16:55:42 +08:00
|
|
|
|
return this._computeTrend(prev, total, higherIsBetter);
|
2018-05-11 11:30:21 +08:00
|
|
|
|
},
|
|
|
|
|
|
2018-05-28 16:55:42 +08:00
|
|
|
|
@computed("prev30Days", "lastThirtyDaysCount", "higher_is_better")
|
|
|
|
|
thirtyDaysTrend(prev30Days, lastThirtyDaysCount, higherIsBetter) {
|
|
|
|
|
return this._computeTrend(prev30Days, lastThirtyDaysCount, higherIsBetter);
|
2018-03-16 05:10:45 +08:00
|
|
|
|
},
|
2013-04-17 06:37:35 +08:00
|
|
|
|
|
2018-05-18 04:44:33 +08:00
|
|
|
|
@computed("type")
|
2018-03-16 05:10:45 +08:00
|
|
|
|
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) {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
const change = this._computeChange(val1, val2);
|
|
|
|
|
|
|
|
|
|
if (isNaN(change) || !isFinite(change)) {
|
2013-04-27 05:13:20 +08:00
|
|
|
|
return null;
|
2018-05-18 04:44:33 +08:00
|
|
|
|
} else if (change > 0) {
|
|
|
|
|
return "+" + change.toFixed(0) + "%";
|
2013-04-27 05:13:20 +08:00
|
|
|
|
} else {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
return change.toFixed(0) + "%";
|
2013-04-27 05:13:20 +08:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2018-05-18 04:44:33 +08:00
|
|
|
|
@computed("prev_period", "currentTotal", "currentAverage")
|
2018-05-11 11:30:21 +08:00
|
|
|
|
trendTitle(prev, currentTotal, currentAverage) {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
let current = this.get("average") ? currentAverage : currentTotal;
|
|
|
|
|
let percent = this.percentChangeString(prev, current);
|
2018-05-11 11:30:21 +08:00
|
|
|
|
|
2018-05-18 04:44:33 +08:00
|
|
|
|
if (this.get("average")) {
|
2018-05-14 09:12:52 +08:00
|
|
|
|
prev = prev ? prev.toFixed(1) : "0";
|
2018-05-18 04:44:33 +08:00
|
|
|
|
if (this.get("percent")) {
|
|
|
|
|
current += "%";
|
|
|
|
|
prev += "%";
|
2018-05-15 08:17:17 +08:00
|
|
|
|
}
|
2018-05-18 04:44:33 +08:00
|
|
|
|
} else {
|
|
|
|
|
prev = number(prev);
|
|
|
|
|
current = number(current);
|
2018-05-11 11:30:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-15 23:03:24 +08:00
|
|
|
|
return I18n.t("admin.dashboard.reports.trend_title", {
|
|
|
|
|
percent,
|
|
|
|
|
prev,
|
|
|
|
|
current
|
|
|
|
|
});
|
2018-05-11 11:30:21 +08:00
|
|
|
|
},
|
|
|
|
|
|
2018-05-18 04:44:33 +08:00
|
|
|
|
changeTitle(valAtT1, valAtT2, prevPeriodString) {
|
|
|
|
|
const change = this.percentChangeString(valAtT1, valAtT2);
|
|
|
|
|
let title = "";
|
2018-06-15 23:03:24 +08:00
|
|
|
|
if (change) {
|
|
|
|
|
title += `${change} change. `;
|
|
|
|
|
}
|
2018-05-29 02:35:22 +08:00
|
|
|
|
title += `Was ${number(valAtT1)} ${prevPeriodString}.`;
|
2013-04-27 05:13:20 +08:00
|
|
|
|
return title;
|
|
|
|
|
},
|
|
|
|
|
|
2018-05-18 04:44:33 +08:00
|
|
|
|
@computed("yesterdayCount")
|
2018-05-03 21:41:41 +08:00
|
|
|
|
yesterdayCountTitle(yesterdayCount) {
|
2018-05-29 02:35:22 +08:00
|
|
|
|
return this.changeTitle(this.valueAt(2), yesterdayCount, "two days ago");
|
2018-03-16 05:10:45 +08:00
|
|
|
|
},
|
2013-04-27 05:13:20 +08:00
|
|
|
|
|
2018-05-18 04:44:33 +08:00
|
|
|
|
@computed("lastSevenDaysCount")
|
|
|
|
|
sevenDaysCountTitle(lastSevenDaysCount) {
|
2018-06-15 23:03:24 +08:00
|
|
|
|
return this.changeTitle(
|
|
|
|
|
this.valueFor(8, 14),
|
|
|
|
|
lastSevenDaysCount,
|
|
|
|
|
"two weeks ago"
|
|
|
|
|
);
|
2018-03-16 05:10:45 +08:00
|
|
|
|
},
|
2013-04-27 05:13:20 +08:00
|
|
|
|
|
2018-05-18 04:44:33 +08:00
|
|
|
|
@computed("prev30Days", "lastThirtyDaysCount")
|
|
|
|
|
thirtyDaysCountTitle(prev30Days, lastThirtyDaysCount) {
|
2018-06-15 23:03:24 +08:00
|
|
|
|
return this.changeTitle(
|
|
|
|
|
prev30Days,
|
|
|
|
|
lastThirtyDaysCount,
|
|
|
|
|
"in the previous 30 day period"
|
|
|
|
|
);
|
2018-03-16 05:10:45 +08:00
|
|
|
|
},
|
|
|
|
|
|
2018-05-18 04:44:33 +08:00
|
|
|
|
@computed("data")
|
2018-03-16 05:10:45 +08:00
|
|
|
|
sortedData(data) {
|
2018-05-18 04:44:33 +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-05-18 04:44:33 +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-05-18 04:44:33 +08:00
|
|
|
|
},
|
2013-04-27 05:13:20 +08:00
|
|
|
|
|
2018-08-01 05:35:13 +08:00
|
|
|
|
@computed("labels")
|
|
|
|
|
computedLabels(labels) {
|
|
|
|
|
return labels.map(label => {
|
|
|
|
|
const type = label.type;
|
|
|
|
|
|
|
|
|
|
let mainProperty;
|
|
|
|
|
if (label.property) mainProperty = label.property;
|
|
|
|
|
else if (type === "user") mainProperty = label.properties["username"];
|
|
|
|
|
else if (type === "topic") mainProperty = label.properties["title"];
|
|
|
|
|
else if (type === "post")
|
|
|
|
|
mainProperty = label.properties["truncated_raw"];
|
|
|
|
|
else mainProperty = label.properties[0];
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
title: label.title,
|
|
|
|
|
sortProperty: label.sort_property || mainProperty,
|
|
|
|
|
mainProperty,
|
|
|
|
|
compute: row => {
|
|
|
|
|
const value = row[mainProperty];
|
|
|
|
|
|
|
|
|
|
if (type === "user") return this._userLabel(label.properties, row);
|
|
|
|
|
if (type === "post") return this._postLabel(label.properties, row);
|
|
|
|
|
if (type === "topic") return this._topicLabel(label.properties, row);
|
|
|
|
|
if (type === "seconds")
|
|
|
|
|
return this._secondsLabel(mainProperty, value);
|
|
|
|
|
if (type === "link") return this._linkLabel(label.properties, row);
|
|
|
|
|
if (type === "percent")
|
|
|
|
|
return this._percentLabel(mainProperty, value);
|
|
|
|
|
if (type === "number" || isNumeric(value)) {
|
|
|
|
|
return this._numberLabel(mainProperty, value);
|
|
|
|
|
}
|
|
|
|
|
if (type === "date") {
|
|
|
|
|
const date = moment(value, "YYYY-MM-DD");
|
|
|
|
|
if (date.isValid())
|
|
|
|
|
return this._dateLabel(mainProperty, value, date);
|
|
|
|
|
}
|
|
|
|
|
if (type === "text") return this._textLabel(mainProperty, value);
|
|
|
|
|
if (!value) return this._undefinedLabel();
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
property: mainProperty,
|
|
|
|
|
value,
|
|
|
|
|
type: type || "string",
|
|
|
|
|
formatedValue: escapeExpression(value)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_undefinedLabel() {
|
|
|
|
|
return {
|
|
|
|
|
value: null,
|
|
|
|
|
formatedValue: "-",
|
|
|
|
|
type: "undefined"
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_userLabel(properties, row) {
|
|
|
|
|
const username = row[properties.username];
|
|
|
|
|
|
|
|
|
|
if (!username) return this._undefinedLabel();
|
|
|
|
|
|
|
|
|
|
const user = Ember.Object.create({
|
|
|
|
|
username,
|
|
|
|
|
name: formatUsername(username),
|
|
|
|
|
avatar_template: row[properties.avatar]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const avatarImg = renderAvatar(user, {
|
2018-08-01 06:57:00 +08:00
|
|
|
|
imageSize: "tiny",
|
2018-08-01 05:35:13 +08:00
|
|
|
|
ignoreTitle: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const href = `/admin/users/${row[properties.id]}/${username}`;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
type: "user",
|
|
|
|
|
property: properties.username,
|
|
|
|
|
value: username,
|
|
|
|
|
formatedValue: `<a href='${href}'>${avatarImg}<span class='username'>${username}</span></a>`
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_topicLabel(properties, row) {
|
|
|
|
|
const topicTitle = row[properties.title];
|
|
|
|
|
const topicId = row[properties.id];
|
|
|
|
|
const href = `/t/-/${topicId}`;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
type: "topic",
|
|
|
|
|
property: properties.title,
|
|
|
|
|
value: topicTitle,
|
|
|
|
|
formatedValue: `<a href='${href}'>${topicTitle}</a>`
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_postLabel(properties, row) {
|
|
|
|
|
const postTitle = row[properties.truncated_raw];
|
|
|
|
|
const postNumber = row[properties.number];
|
|
|
|
|
const topicId = row[properties.topic_id];
|
|
|
|
|
const href = `/t/-/${topicId}/${postNumber}`;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
type: "post",
|
|
|
|
|
property: properties.title,
|
|
|
|
|
value: postTitle,
|
|
|
|
|
formatedValue: `<a href='${href}'>${postTitle}</a>`
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_secondsLabel(property, value) {
|
|
|
|
|
return {
|
|
|
|
|
value,
|
|
|
|
|
property,
|
|
|
|
|
countable: true,
|
|
|
|
|
type: "seconds",
|
|
|
|
|
formatedValue: durationTiny(value)
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_percentLabel(property, value) {
|
|
|
|
|
return {
|
|
|
|
|
type: "percent",
|
|
|
|
|
property,
|
|
|
|
|
value,
|
|
|
|
|
formatedValue: `${value}%`
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_numberLabel(property, value) {
|
|
|
|
|
return {
|
|
|
|
|
type: "number",
|
|
|
|
|
countable: true,
|
|
|
|
|
property,
|
|
|
|
|
value,
|
|
|
|
|
formatedValue: number(value)
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_dateLabel(property, value, date) {
|
|
|
|
|
return {
|
|
|
|
|
type: "date",
|
|
|
|
|
property,
|
|
|
|
|
value,
|
|
|
|
|
formatedValue: date.format("LL")
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_textLabel(property, value) {
|
|
|
|
|
const escaped = escapeExpression(value);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
type: "text",
|
|
|
|
|
property,
|
|
|
|
|
value,
|
|
|
|
|
formatedValue: escaped
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_linkLabel(properties, row) {
|
|
|
|
|
const property = properties[0];
|
|
|
|
|
const value = row[property];
|
|
|
|
|
return {
|
|
|
|
|
type: "link",
|
|
|
|
|
property,
|
|
|
|
|
value,
|
|
|
|
|
formatedValue: `<a href="${escapeExpression(
|
|
|
|
|
row[properties[1]]
|
|
|
|
|
)}">${escapeExpression(value)}</a>`
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
2018-05-18 04:44:33 +08:00
|
|
|
|
_computeChange(valAtT1, valAtT2) {
|
|
|
|
|
return ((valAtT2 - valAtT1) / valAtT1) * 100;
|
|
|
|
|
},
|
|
|
|
|
|
2018-05-28 16:55:42 +08:00
|
|
|
|
_computeTrend(valAtT1, valAtT2, higherIsBetter) {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
const change = this._computeChange(valAtT1, valAtT2);
|
|
|
|
|
|
|
|
|
|
if (change > 50) {
|
|
|
|
|
return higherIsBetter ? "high-trending-up" : "high-trending-down";
|
2018-05-28 16:55:42 +08:00
|
|
|
|
} else if (change > 2) {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
return higherIsBetter ? "trending-up" : "trending-down";
|
2018-05-28 16:55:42 +08:00
|
|
|
|
} else if (change <= 2 && change >= -2) {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
return "no-change";
|
|
|
|
|
} else if (change < -50) {
|
|
|
|
|
return higherIsBetter ? "high-trending-down" : "high-trending-up";
|
2018-05-28 16:55:42 +08:00
|
|
|
|
} else if (change < -2) {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
return higherIsBetter ? "trending-down" : "trending-up";
|
|
|
|
|
}
|
2018-05-25 18:09:30 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_iconForTrend(trend, higherIsBetter) {
|
|
|
|
|
switch (trend) {
|
|
|
|
|
case "trending-up":
|
|
|
|
|
return higherIsBetter ? "angle-up" : "angle-down";
|
|
|
|
|
case "trending-down":
|
|
|
|
|
return higherIsBetter ? "angle-down" : "angle-up";
|
|
|
|
|
case "high-trending-up":
|
|
|
|
|
return higherIsBetter ? "angle-double-up" : "angle-double-down";
|
|
|
|
|
case "high-trending-down":
|
|
|
|
|
return higherIsBetter ? "angle-double-down" : "angle-double-up";
|
|
|
|
|
default:
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2018-05-18 04:44:33 +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({
|
2018-07-20 02:33:11 +08:00
|
|
|
|
fillMissingDates(report, options = {}) {
|
|
|
|
|
const dataField = options.dataField || "data";
|
|
|
|
|
const filledField = options.filledField || "data";
|
|
|
|
|
const startDate = options.startDate || "start_date";
|
|
|
|
|
const endDate = options.endDate || "end_date";
|
|
|
|
|
|
|
|
|
|
if (_.isArray(report[dataField])) {
|
2018-06-15 23:03:24 +08:00
|
|
|
|
const startDateFormatted = moment
|
2018-07-20 02:33:11 +08:00
|
|
|
|
.utc(report[startDate])
|
2018-06-15 23:03:24 +08:00
|
|
|
|
.locale("en")
|
|
|
|
|
.format("YYYY-MM-DD");
|
|
|
|
|
const endDateFormatted = moment
|
2018-07-20 02:33:11 +08:00
|
|
|
|
.utc(report[endDate])
|
2018-06-15 23:03:24 +08:00
|
|
|
|
.locale("en")
|
|
|
|
|
.format("YYYY-MM-DD");
|
2018-07-20 02:33:11 +08:00
|
|
|
|
report[filledField] = fillMissingDates(
|
|
|
|
|
JSON.parse(JSON.stringify(report[dataField])),
|
2018-06-15 23:03:24 +08:00
|
|
|
|
startDateFormatted,
|
|
|
|
|
endDateFormatted
|
|
|
|
|
);
|
2018-05-11 11:30:21 +08:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
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-07-20 02:33:11 +08:00
|
|
|
|
// don’t fill for large multi column tables
|
|
|
|
|
// which are not date based
|
|
|
|
|
const modes = json.report.modes;
|
|
|
|
|
if (modes.length !== 1 && modes[0] !== "table") {
|
|
|
|
|
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
|
2018-06-15 23:03:24 +08:00
|
|
|
|
const related = Report.create({
|
|
|
|
|
type: json.report.related_report.type
|
|
|
|
|
});
|
2018-03-16 05:10:45 +08:00
|
|
|
|
related.setProperties(json.report.related_report);
|
2018-05-18 04:44:33 +08:00
|
|
|
|
model.set("relatedReport", related);
|
2018-03-16 05:10:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
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;
|