fix: Consider timezone in the reports (#4027)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
parent
4ca66c1195
commit
9b615f11f1
7 changed files with 73 additions and 46 deletions
|
@ -7,6 +7,9 @@ class V2::ReportBuilder
|
||||||
def initialize(account, params)
|
def initialize(account, params)
|
||||||
@account = account
|
@account = account
|
||||||
@params = params
|
@params = params
|
||||||
|
|
||||||
|
timezone_offset = (params[:timezone_offset] || 0).to_f
|
||||||
|
@timezone = ActiveSupport::TimeZone[timezone_offset]&.name
|
||||||
end
|
end
|
||||||
|
|
||||||
def timeseries
|
def timeseries
|
||||||
|
@ -64,60 +67,58 @@ class V2::ReportBuilder
|
||||||
@team ||= account.teams.find(params[:id])
|
@team ||= account.teams.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_grouped_values(object_scope)
|
||||||
|
object_scope.group_by_period(
|
||||||
|
params[:group_by] || DEFAULT_GROUP_BY,
|
||||||
|
:created_at,
|
||||||
|
default_value: 0,
|
||||||
|
range: range,
|
||||||
|
permit: %w[day week month year],
|
||||||
|
time_zone: @timezone
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def conversations_count
|
def conversations_count
|
||||||
scope.conversations
|
(get_grouped_values scope.conversations).count
|
||||||
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
|
|
||||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
|
||||||
.count
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def incoming_messages_count
|
def incoming_messages_count
|
||||||
scope.messages.incoming.unscope(:order)
|
(get_grouped_values scope.messages.incoming.unscope(:order)).count
|
||||||
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
|
|
||||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
|
||||||
.count
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def outgoing_messages_count
|
def outgoing_messages_count
|
||||||
scope.messages.outgoing.unscope(:order)
|
(get_grouped_values scope.messages.outgoing.unscope(:order)).count
|
||||||
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
|
|
||||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
|
||||||
.count
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolutions_count
|
def resolutions_count
|
||||||
scope.conversations
|
(get_grouped_values scope.conversations.resolved).count
|
||||||
.resolved
|
|
||||||
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
|
|
||||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
|
||||||
.count
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def avg_first_response_time
|
def avg_first_response_time
|
||||||
scope.events
|
(get_grouped_values scope.events.where(name: 'first_response')).average(:value)
|
||||||
.where(name: 'first_response')
|
|
||||||
.group_by_day(:created_at, range: range, default_value: 0)
|
|
||||||
.average(:value)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def avg_resolution_time
|
def avg_resolution_time
|
||||||
scope.events.where(name: 'conversation_resolved')
|
(get_grouped_values scope.events.where(name: 'conversation_resolved')).average(:value)
|
||||||
.group_by_day(:created_at, range: range, default_value: 0)
|
|
||||||
.average(:value)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Taking average of average is not too accurate
|
|
||||||
# https://en.wikipedia.org/wiki/Simpson's_paradox
|
|
||||||
# TODO: Will optimize this later
|
|
||||||
def avg_resolution_time_summary
|
def avg_resolution_time_summary
|
||||||
return 0 if avg_resolution_time.values.empty?
|
avg_rt = scope.events
|
||||||
|
.where(name: 'conversation_resolved', created_at: range)
|
||||||
|
.average(:value)
|
||||||
|
|
||||||
(avg_resolution_time.values.sum / avg_resolution_time.values.length)
|
return 0 if avg_rt.blank?
|
||||||
|
|
||||||
|
avg_rt
|
||||||
end
|
end
|
||||||
|
|
||||||
def avg_first_response_time_summary
|
def avg_first_response_time_summary
|
||||||
return 0 if avg_first_response_time.values.empty?
|
avg_frt = scope.events
|
||||||
|
.where(name: 'first_response', created_at: range)
|
||||||
|
.average(:value)
|
||||||
|
|
||||||
(avg_first_response_time.values.sum / avg_first_response_time.values.length)
|
return 0 if avg_frt.blank?
|
||||||
|
|
||||||
|
avg_frt
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -58,7 +58,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||||
since: params[:since],
|
since: params[:since],
|
||||||
until: params[:until],
|
until: params[:until],
|
||||||
id: params[:id],
|
id: params[:id],
|
||||||
group_by: params[:group_by]
|
group_by: params[:group_by],
|
||||||
|
timezone_offset: params[:timezone_offset]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
/* global axios */
|
/* global axios */
|
||||||
import ApiClient from './ApiClient';
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
const getTimeOffset = () => -new Date().getTimezoneOffset() / 60;
|
||||||
|
|
||||||
class ReportsAPI extends ApiClient {
|
class ReportsAPI extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
||||||
|
@ -8,13 +10,27 @@ class ReportsAPI extends ApiClient {
|
||||||
|
|
||||||
getReports(metric, since, until, type = 'account', id, group_by) {
|
getReports(metric, since, until, type = 'account', id, group_by) {
|
||||||
return axios.get(`${this.url}`, {
|
return axios.get(`${this.url}`, {
|
||||||
params: { metric, since, until, type, id, group_by },
|
params: {
|
||||||
|
metric,
|
||||||
|
since,
|
||||||
|
until,
|
||||||
|
type,
|
||||||
|
id,
|
||||||
|
group_by,
|
||||||
|
timezone_offset: getTimeOffset(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getSummary(since, until, type = 'account', id, group_by) {
|
getSummary(since, until, type = 'account', id, group_by) {
|
||||||
return axios.get(`${this.url}/summary`, {
|
return axios.get(`${this.url}/summary`, {
|
||||||
params: { since, until, type, id, group_by },
|
params: {
|
||||||
|
since,
|
||||||
|
until,
|
||||||
|
type,
|
||||||
|
id,
|
||||||
|
group_by,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ describe('#Reports API', () => {
|
||||||
since: 1621103400,
|
since: 1621103400,
|
||||||
until: 1621621800,
|
until: 1621621800,
|
||||||
type: 'account',
|
type: 'account',
|
||||||
|
timezone_offset: -0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -50,6 +50,7 @@ import subDays from 'date-fns/subDays';
|
||||||
import startOfDay from 'date-fns/startOfDay';
|
import startOfDay from 'date-fns/startOfDay';
|
||||||
import getUnixTime from 'date-fns/getUnixTime';
|
import getUnixTime from 'date-fns/getUnixTime';
|
||||||
import { GROUP_BY_FILTER } from '../constants';
|
import { GROUP_BY_FILTER } from '../constants';
|
||||||
|
import endOfDay from 'date-fns/endOfDay';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -79,9 +80,9 @@ export default {
|
||||||
},
|
},
|
||||||
to() {
|
to() {
|
||||||
if (this.isDateRangeSelected) {
|
if (this.isDateRangeSelected) {
|
||||||
return this.fromCustomDate(this.customDateRange[1]);
|
return this.toCustomDate(this.customDateRange[1]);
|
||||||
}
|
}
|
||||||
return this.fromCustomDate(new Date());
|
return this.toCustomDate(new Date());
|
||||||
},
|
},
|
||||||
from() {
|
from() {
|
||||||
if (this.isDateRangeSelected) {
|
if (this.isDateRangeSelected) {
|
||||||
|
@ -134,6 +135,9 @@ export default {
|
||||||
fromCustomDate(date) {
|
fromCustomDate(date) {
|
||||||
return getUnixTime(startOfDay(date));
|
return getUnixTime(startOfDay(date));
|
||||||
},
|
},
|
||||||
|
toCustomDate(date) {
|
||||||
|
return getUnixTime(endOfDay(date));
|
||||||
|
},
|
||||||
changeDateSelection(selectedRange) {
|
changeDateSelection(selectedRange) {
|
||||||
this.currentDateRangeSelection = selectedRange;
|
this.currentDateRangeSelection = selectedRange;
|
||||||
this.onDateRangeChange();
|
this.onDateRangeChange();
|
||||||
|
|
|
@ -148,13 +148,15 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
import endOfDay from 'date-fns/endOfDay';
|
||||||
const CUSTOM_DATE_RANGE_ID = 5;
|
|
||||||
import subDays from 'date-fns/subDays';
|
|
||||||
import startOfDay from 'date-fns/startOfDay';
|
|
||||||
import getUnixTime from 'date-fns/getUnixTime';
|
import getUnixTime from 'date-fns/getUnixTime';
|
||||||
|
import startOfDay from 'date-fns/startOfDay';
|
||||||
|
import subDays from 'date-fns/subDays';
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
|
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
||||||
|
|
||||||
import { GROUP_BY_FILTER } from '../constants';
|
import { GROUP_BY_FILTER } from '../constants';
|
||||||
|
const CUSTOM_DATE_RANGE_ID = 5;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -194,9 +196,9 @@ export default {
|
||||||
},
|
},
|
||||||
to() {
|
to() {
|
||||||
if (this.isDateRangeSelected) {
|
if (this.isDateRangeSelected) {
|
||||||
return this.fromCustomDate(this.customDateRange[1]);
|
return this.toCustomDate(this.customDateRange[1]);
|
||||||
}
|
}
|
||||||
return this.fromCustomDate(new Date());
|
return this.toCustomDate(new Date());
|
||||||
},
|
},
|
||||||
from() {
|
from() {
|
||||||
if (this.isDateRangeSelected) {
|
if (this.isDateRangeSelected) {
|
||||||
|
@ -253,6 +255,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onDateRangeChange() {
|
onDateRangeChange() {
|
||||||
|
console.log(this.from, this.to);
|
||||||
this.$emit('date-range-change', {
|
this.$emit('date-range-change', {
|
||||||
from: this.from,
|
from: this.from,
|
||||||
to: this.to,
|
to: this.to,
|
||||||
|
@ -262,6 +265,9 @@ export default {
|
||||||
fromCustomDate(date) {
|
fromCustomDate(date) {
|
||||||
return getUnixTime(startOfDay(date));
|
return getUnixTime(startOfDay(date));
|
||||||
},
|
},
|
||||||
|
toCustomDate(date) {
|
||||||
|
return getUnixTime(endOfDay(date));
|
||||||
|
},
|
||||||
changeDateSelection(selectedRange) {
|
changeDateSelection(selectedRange) {
|
||||||
this.currentDateRangeSelection = selectedRange;
|
this.currentDateRangeSelection = selectedRange;
|
||||||
this.onDateRangeChange();
|
this.onDateRangeChange();
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
/* eslint no-console: 0 */
|
/* eslint no-console: 0 */
|
||||||
/* eslint no-param-reassign: 0 */
|
/* eslint no-param-reassign: 0 */
|
||||||
/* eslint no-shadow: 0 */
|
/* eslint no-shadow: 0 */
|
||||||
import compareAsc from 'date-fns/compareAsc';
|
|
||||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
|
||||||
|
|
||||||
import * as types from '../mutation-types';
|
import * as types from '../mutation-types';
|
||||||
import Report from '../../api/reports';
|
import Report from '../../api/reports';
|
||||||
|
|
||||||
|
@ -48,7 +45,8 @@ export const actions = {
|
||||||
).then(accountReport => {
|
).then(accountReport => {
|
||||||
let { data } = accountReport;
|
let { data } = accountReport;
|
||||||
data = data.filter(
|
data = data.filter(
|
||||||
el => compareAsc(new Date(), fromUnixTime(el.timestamp)) > -1
|
el =>
|
||||||
|
reportObj.to - el.timestamp > 0 && el.timestamp - reportObj.from >= 0
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
reportObj.metric === 'avg_first_response_time' ||
|
reportObj.metric === 'avg_first_response_time' ||
|
||||||
|
|
Loading…
Reference in a new issue