feat: Update reports UI to make it better (#7544)
This commit is contained in:
@@ -58,31 +58,3 @@
|
|||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-bar {
|
|
||||||
@include background-white;
|
|
||||||
@include border-light;
|
|
||||||
margin: var(--space-minus-micro) 0;
|
|
||||||
padding: var(--space-small) var(--space-medium);
|
|
||||||
|
|
||||||
.chart-container {
|
|
||||||
@include flex;
|
|
||||||
@include flex-align(center, middle);
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
div {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
color: $color-gray;
|
|
||||||
font-size: var(--font-size-default);
|
|
||||||
margin: var(--space-jumbo);
|
|
||||||
}
|
|
||||||
|
|
||||||
.business-hours {
|
|
||||||
margin: var(--space-normal);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,18 +22,6 @@
|
|||||||
margin: 0 var(--space-small);
|
margin: 0 var(--space-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
.business-hours {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
margin-left: auto;
|
|
||||||
padding-right: var(--space-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.business-hours-text {
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
margin: 0 var(--space-small);
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch {
|
.switch {
|
||||||
margin-bottom: var(--space-zero);
|
margin-bottom: var(--space-zero);
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { Bar } from 'vue-chartjs';
|
import { Bar } from 'vue-chartjs';
|
||||||
|
|
||||||
const fontFamily =
|
const fontFamily =
|
||||||
'-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
'PlusJakarta,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
||||||
|
|
||||||
const defaultChartOptions = {
|
const defaultChartOptions = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
legend: {
|
legend: {
|
||||||
|
display: false,
|
||||||
labels: {
|
labels: {
|
||||||
fontFamily,
|
fontFamily,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
datasets: {
|
datasets: {
|
||||||
bar: {
|
bar: {
|
||||||
barPercentage: 1.0,
|
barPercentage: 1.0,
|
||||||
@@ -46,11 +50,11 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
collection: {
|
collection: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
chartOptions: {
|
chartOptions: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
@@ -12,11 +12,11 @@
|
|||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"INCOMING_MESSAGES": {
|
"INCOMING_MESSAGES": {
|
||||||
"NAME": "Incoming Messages",
|
"NAME": "Messages received",
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"OUTGOING_MESSAGES": {
|
"OUTGOING_MESSAGES": {
|
||||||
"NAME": "Outgoing Messages",
|
"NAME": "Messages sent",
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"FIRST_RESPONSE_TIME": {
|
"FIRST_RESPONSE_TIME": {
|
||||||
@@ -93,7 +93,6 @@
|
|||||||
{ "id": 3, "groupBy": "Month" }
|
{ "id": 3, "groupBy": "Month" }
|
||||||
],
|
],
|
||||||
"GROUP_BY_YEAR_OPTIONS": [
|
"GROUP_BY_YEAR_OPTIONS": [
|
||||||
{ "id": 1, "groupBy": "Day" },
|
|
||||||
{ "id": 2, "groupBy": "Week" },
|
{ "id": 2, "groupBy": "Week" },
|
||||||
{ "id": 3, "groupBy": "Month" },
|
{ "id": 3, "groupBy": "Month" },
|
||||||
{ "id": 4, "groupBy": "Year" }
|
{ "id": 4, "groupBy": "Year" }
|
||||||
|
|||||||
@@ -10,42 +10,36 @@ export default {
|
|||||||
calculateTrend() {
|
calculateTrend() {
|
||||||
return metric_key => {
|
return metric_key => {
|
||||||
if (!this.accountSummary.previous[metric_key]) return 0;
|
if (!this.accountSummary.previous[metric_key]) return 0;
|
||||||
|
const diff =
|
||||||
|
this.accountSummary[metric_key] -
|
||||||
|
this.accountSummary.previous[metric_key];
|
||||||
return Math.round(
|
return Math.round(
|
||||||
((this.accountSummary[metric_key] -
|
(diff / this.accountSummary.previous[metric_key]) * 100
|
||||||
this.accountSummary.previous[metric_key]) /
|
|
||||||
this.accountSummary.previous[metric_key]) *
|
|
||||||
100
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
displayMetric() {
|
|
||||||
return metric_key => {
|
|
||||||
if (this.isAverageMetricType(metric_key)) {
|
|
||||||
return formatTime(this.accountSummary[metric_key]);
|
|
||||||
}
|
|
||||||
return this.accountSummary[metric_key];
|
|
||||||
};
|
|
||||||
},
|
|
||||||
displayInfoText() {
|
|
||||||
return metric_key => {
|
|
||||||
if (this.metrics[this.currentSelection].KEY !== metric_key) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
if (this.isAverageMetricType(metric_key)) {
|
|
||||||
const total = this.accountReport.data
|
|
||||||
.map(item => item.count)
|
|
||||||
.reduce((prev, curr) => prev + curr, 0);
|
|
||||||
return `${this.metrics[this.currentSelection].INFO_TEXT} ${total}`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
},
|
|
||||||
isAverageMetricType() {
|
|
||||||
return metric_key => {
|
|
||||||
return ['avg_first_response_time', 'avg_resolution_time'].includes(
|
|
||||||
metric_key
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
displayMetric(key) {
|
||||||
|
if (this.isAverageMetricType(key)) {
|
||||||
|
return formatTime(this.accountSummary[key]);
|
||||||
|
}
|
||||||
|
return Number(this.accountSummary[key] || '').toLocaleString();
|
||||||
|
},
|
||||||
|
displayInfoText(key) {
|
||||||
|
if (this.metrics[this.currentSelection].KEY !== key) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (this.isAverageMetricType(key)) {
|
||||||
|
const total = this.accountReport.data
|
||||||
|
.map(item => item.count)
|
||||||
|
.reduce((prev, curr) => prev + curr, 0);
|
||||||
|
return `${this.metrics[this.currentSelection].INFO_TEXT} ${total}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
isAverageMetricType(key) {
|
||||||
|
return ['avg_first_response_time', 'avg_resolution_time'].includes(key);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ describe('reportMixin', () => {
|
|||||||
mixins: [reportMixin],
|
mixins: [reportMixin],
|
||||||
};
|
};
|
||||||
const wrapper = shallowMount(Component, { store, localVue });
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
expect(wrapper.vm.displayMetric('conversations_count')).toEqual(5);
|
expect(wrapper.vm.displayMetric('conversations_count')).toEqual('5,000');
|
||||||
expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual(
|
expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual(
|
||||||
'3 Min 18 Sec'
|
'3 Min 18 Sec'
|
||||||
);
|
);
|
||||||
@@ -36,7 +36,7 @@ describe('reportMixin', () => {
|
|||||||
mixins: [reportMixin],
|
mixins: [reportMixin],
|
||||||
};
|
};
|
||||||
const wrapper = shallowMount(Component, { store, localVue });
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(25);
|
expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(124900);
|
||||||
expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0);
|
expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export default {
|
|||||||
summary: {
|
summary: {
|
||||||
avg_first_response_time: '198.6666666666667',
|
avg_first_response_time: '198.6666666666667',
|
||||||
avg_resolution_time: '208.3333333333333',
|
avg_resolution_time: '208.3333333333333',
|
||||||
conversations_count: 5,
|
conversations_count: 5000,
|
||||||
incoming_messages_count: 5,
|
incoming_messages_count: 5,
|
||||||
outgoing_messages_count: 3,
|
outgoing_messages_count: 3,
|
||||||
previous: {
|
previous: {
|
||||||
|
|||||||
@@ -13,36 +13,7 @@
|
|||||||
:show-group-by-filter="true"
|
:show-group-by-filter="true"
|
||||||
@filter-change="onFilterChange"
|
@filter-change="onFilterChange"
|
||||||
/>
|
/>
|
||||||
<div class="row">
|
<report-container :group-by="groupBy" />
|
||||||
<woot-report-stats-card
|
|
||||||
v-for="(metric, index) in metrics"
|
|
||||||
:key="metric.NAME"
|
|
||||||
:desc="metric.DESC"
|
|
||||||
:heading="metric.NAME"
|
|
||||||
:info-text="displayInfoText(metric.KEY)"
|
|
||||||
:index="index"
|
|
||||||
:on-click="changeSelection"
|
|
||||||
:point="displayMetric(metric.KEY)"
|
|
||||||
:trend="calculateTrend(metric.KEY)"
|
|
||||||
:selected="index === currentSelection"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="report-bar">
|
|
||||||
<woot-loading-state
|
|
||||||
v-if="accountReport.isFetching"
|
|
||||||
:message="$t('REPORT.LOADING_CHART')"
|
|
||||||
/>
|
|
||||||
<div v-else class="chart-container">
|
|
||||||
<woot-bar
|
|
||||||
v-if="accountReport.data.length"
|
|
||||||
:collection="collection"
|
|
||||||
:chart-options="chartOptions"
|
|
||||||
/>
|
|
||||||
<span v-else class="empty-state">
|
|
||||||
{{ $t('REPORT.NO_ENOUGH_DATA') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -51,11 +22,11 @@ import { mapGetters } from 'vuex';
|
|||||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||||
import format from 'date-fns/format';
|
import format from 'date-fns/format';
|
||||||
import ReportFilterSelector from './components/FilterSelector';
|
import ReportFilterSelector from './components/FilterSelector';
|
||||||
import { GROUP_BY_FILTER, METRIC_CHART } from './constants';
|
import { GROUP_BY_FILTER } from './constants';
|
||||||
import reportMixin from 'dashboard/mixins/reportMixin';
|
import reportMixin from 'dashboard/mixins/reportMixin';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import { formatTime } from '@chatwoot/utils';
|
|
||||||
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||||
|
import ReportContainer from './ReportContainer.vue';
|
||||||
|
|
||||||
const REPORTS_KEYS = {
|
const REPORTS_KEYS = {
|
||||||
CONVERSATIONS: 'conversations_count',
|
CONVERSATIONS: 'conversations_count',
|
||||||
@@ -70,13 +41,13 @@ export default {
|
|||||||
name: 'ConversationReports',
|
name: 'ConversationReports',
|
||||||
components: {
|
components: {
|
||||||
ReportFilterSelector,
|
ReportFilterSelector,
|
||||||
|
ReportContainer,
|
||||||
},
|
},
|
||||||
mixins: [reportMixin, alertMixin],
|
mixins: [reportMixin, alertMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
from: 0,
|
from: 0,
|
||||||
to: 0,
|
to: 0,
|
||||||
currentSelection: 0,
|
|
||||||
groupBy: GROUP_BY_FILTER[1],
|
groupBy: GROUP_BY_FILTER[1],
|
||||||
businessHours: false,
|
businessHours: false,
|
||||||
};
|
};
|
||||||
@@ -86,104 +57,6 @@ export default {
|
|||||||
accountSummary: 'getAccountSummary',
|
accountSummary: 'getAccountSummary',
|
||||||
accountReport: 'getAccountReports',
|
accountReport: 'getAccountReports',
|
||||||
}),
|
}),
|
||||||
collection() {
|
|
||||||
if (this.accountReport.isFetching) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
if (!this.accountReport.data.length) return {};
|
|
||||||
const labels = this.accountReport.data.map(element => {
|
|
||||||
if (this.groupBy?.period === GROUP_BY_FILTER[2].period) {
|
|
||||||
let week_date = new Date(fromUnixTime(element.timestamp));
|
|
||||||
const first_day = week_date.getDate() - week_date.getDay();
|
|
||||||
const last_day = first_day + 6;
|
|
||||||
|
|
||||||
const week_first_date = new Date(week_date.setDate(first_day));
|
|
||||||
const week_last_date = new Date(week_date.setDate(last_day));
|
|
||||||
|
|
||||||
return `${format(week_first_date, 'dd/MM/yy')} - ${format(
|
|
||||||
week_last_date,
|
|
||||||
'dd/MM/yy'
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
if (this.groupBy?.period === GROUP_BY_FILTER[3].period) {
|
|
||||||
return format(fromUnixTime(element.timestamp), 'MMM-yyyy');
|
|
||||||
}
|
|
||||||
if (this.groupBy?.period === GROUP_BY_FILTER[4].period) {
|
|
||||||
return format(fromUnixTime(element.timestamp), 'yyyy');
|
|
||||||
}
|
|
||||||
return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy');
|
|
||||||
});
|
|
||||||
|
|
||||||
const datasets = METRIC_CHART[
|
|
||||||
this.metrics[this.currentSelection].KEY
|
|
||||||
].datasets.map(dataset => {
|
|
||||||
switch (dataset.type) {
|
|
||||||
case 'bar':
|
|
||||||
return {
|
|
||||||
...dataset,
|
|
||||||
yAxisID: 'y-left',
|
|
||||||
label: this.metrics[this.currentSelection].NAME,
|
|
||||||
data: this.accountReport.data.map(element => element.value),
|
|
||||||
};
|
|
||||||
case 'line':
|
|
||||||
return {
|
|
||||||
...dataset,
|
|
||||||
yAxisID: 'y-right',
|
|
||||||
label: this.metrics[0].NAME,
|
|
||||||
data: this.accountReport.data.map(element => element.count),
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return dataset;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels,
|
|
||||||
datasets,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
chartOptions() {
|
|
||||||
let tooltips = {};
|
|
||||||
if (this.isAverageMetricType(this.metrics[this.currentSelection].KEY)) {
|
|
||||||
tooltips.callbacks = {
|
|
||||||
label: tooltipItem => {
|
|
||||||
return this.$t(this.metrics[this.currentSelection].TOOLTIP_TEXT, {
|
|
||||||
metricValue: formatTime(tooltipItem.yLabel),
|
|
||||||
conversationCount: this.accountReport.data[tooltipItem.index]
|
|
||||||
.count,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
scales: METRIC_CHART[this.metrics[this.currentSelection].KEY].scales,
|
|
||||||
tooltips: tooltips,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
metrics() {
|
|
||||||
const reportKeys = [
|
|
||||||
'CONVERSATIONS',
|
|
||||||
'INCOMING_MESSAGES',
|
|
||||||
'OUTGOING_MESSAGES',
|
|
||||||
'FIRST_RESPONSE_TIME',
|
|
||||||
'RESOLUTION_TIME',
|
|
||||||
'RESOLUTION_COUNT',
|
|
||||||
];
|
|
||||||
const infoText = {
|
|
||||||
FIRST_RESPONSE_TIME: this.$t(
|
|
||||||
`REPORT.METRICS.FIRST_RESPONSE_TIME.INFO_TEXT`
|
|
||||||
),
|
|
||||||
RESOLUTION_TIME: this.$t(`REPORT.METRICS.RESOLUTION_TIME.INFO_TEXT`),
|
|
||||||
};
|
|
||||||
return reportKeys.map(key => ({
|
|
||||||
NAME: this.$t(`REPORT.METRICS.${key}.NAME`),
|
|
||||||
KEY: REPORTS_KEYS[key],
|
|
||||||
DESC: this.$t(`REPORT.METRICS.${key}.DESC`),
|
|
||||||
INFO_TEXT: infoText[key],
|
|
||||||
TOOLTIP_TEXT: `REPORT.METRICS.${key}.TOOLTIP_TEXT`,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchAllData() {
|
fetchAllData() {
|
||||||
@@ -198,14 +71,23 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchChartData() {
|
fetchChartData() {
|
||||||
try {
|
[
|
||||||
this.$store.dispatch('fetchAccountReport', {
|
'CONVERSATIONS',
|
||||||
metric: this.metrics[this.currentSelection].KEY,
|
'INCOMING_MESSAGES',
|
||||||
...this.getRequestPayload(),
|
'OUTGOING_MESSAGES',
|
||||||
});
|
'FIRST_RESPONSE_TIME',
|
||||||
} catch {
|
'RESOLUTION_TIME',
|
||||||
this.showAlert(this.$t('REPORT.DATA_FETCHING_FAILED'));
|
'RESOLUTION_COUNT',
|
||||||
}
|
].forEach(async key => {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('fetchAccountReport', {
|
||||||
|
metric: REPORTS_KEYS[key],
|
||||||
|
...this.getRequestPayload(),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
this.showAlert(this.$t('REPORT.DATA_FETCHING_FAILED'));
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
getRequestPayload() {
|
getRequestPayload() {
|
||||||
const { from, to, groupBy, businessHours } = this;
|
const { from, to, groupBy, businessHours } = this;
|
||||||
@@ -225,10 +107,6 @@ export default {
|
|||||||
)}.csv`;
|
)}.csv`;
|
||||||
this.$store.dispatch('downloadAgentReports', { from, to, fileName });
|
this.$store.dispatch('downloadAgentReports', { from, to, fileName });
|
||||||
},
|
},
|
||||||
changeSelection(index) {
|
|
||||||
this.currentSelection = index;
|
|
||||||
this.fetchChartData();
|
|
||||||
},
|
|
||||||
onFilterChange({ from, to, groupBy, businessHours }) {
|
onFilterChange({ from, to, groupBy, businessHours }) {
|
||||||
this.from = from;
|
this.from = from;
|
||||||
this.to = to;
|
this.to = to;
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 bg-white dark:bg-slate-800 p-2 border border-slate-100 dark:border-slate-700 rounded-md"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="metric in metrics"
|
||||||
|
:key="metric.KEY"
|
||||||
|
class="p-4 rounded-md mb-3"
|
||||||
|
>
|
||||||
|
<chart-stats :metric="metric" />
|
||||||
|
<div class="mt-4 h-72">
|
||||||
|
<woot-loading-state
|
||||||
|
v-if="accountReport.isFetching[metric.KEY]"
|
||||||
|
class="text-xs"
|
||||||
|
:message="$t('REPORT.LOADING_CHART')"
|
||||||
|
/>
|
||||||
|
<div v-else class="h-72 flex items-center justify-center">
|
||||||
|
<woot-bar
|
||||||
|
v-if="accountReport.data[metric.KEY].length"
|
||||||
|
:collection="getCollection(metric)"
|
||||||
|
:chart-options="getChartOptions(metric)"
|
||||||
|
class="h-72 w-full"
|
||||||
|
/>
|
||||||
|
<span v-else class="text-sm text-slate-600">
|
||||||
|
{{ $t('REPORT.NO_ENOUGH_DATA') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { GROUP_BY_FILTER, METRIC_CHART } from './constants';
|
||||||
|
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import { formatTime } from '@chatwoot/utils';
|
||||||
|
import reportMixin from 'dashboard/mixins/reportMixin';
|
||||||
|
import ChartStats from './components/ChartElements/ChartStats.vue';
|
||||||
|
const REPORTS_KEYS = {
|
||||||
|
CONVERSATIONS: 'conversations_count',
|
||||||
|
INCOMING_MESSAGES: 'incoming_messages_count',
|
||||||
|
OUTGOING_MESSAGES: 'outgoing_messages_count',
|
||||||
|
FIRST_RESPONSE_TIME: 'avg_first_response_time',
|
||||||
|
RESOLUTION_TIME: 'avg_resolution_time',
|
||||||
|
RESOLUTION_COUNT: 'resolutions_count',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { ChartStats },
|
||||||
|
mixins: [reportMixin],
|
||||||
|
props: {
|
||||||
|
groupBy: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
metrics() {
|
||||||
|
const reportKeys = [
|
||||||
|
'CONVERSATIONS',
|
||||||
|
'FIRST_RESPONSE_TIME',
|
||||||
|
'RESOLUTION_TIME',
|
||||||
|
'RESOLUTION_COUNT',
|
||||||
|
'INCOMING_MESSAGES',
|
||||||
|
'OUTGOING_MESSAGES',
|
||||||
|
];
|
||||||
|
const infoText = {
|
||||||
|
FIRST_RESPONSE_TIME: this.$t(
|
||||||
|
`REPORT.METRICS.FIRST_RESPONSE_TIME.INFO_TEXT`
|
||||||
|
),
|
||||||
|
RESOLUTION_TIME: this.$t(`REPORT.METRICS.RESOLUTION_TIME.INFO_TEXT`),
|
||||||
|
};
|
||||||
|
return reportKeys.map(key => ({
|
||||||
|
NAME: this.$t(`REPORT.METRICS.${key}.NAME`),
|
||||||
|
KEY: REPORTS_KEYS[key],
|
||||||
|
DESC: this.$t(`REPORT.METRICS.${key}.DESC`),
|
||||||
|
INFO_TEXT: infoText[key],
|
||||||
|
TOOLTIP_TEXT: `REPORT.METRICS.${key}.TOOLTIP_TEXT`,
|
||||||
|
trend: this.calculateTrend(REPORTS_KEYS[key]),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getCollection(metric) {
|
||||||
|
if (!this.accountReport.data[metric.KEY]) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const data = this.accountReport.data[metric.KEY];
|
||||||
|
const labels = data.map(element => {
|
||||||
|
if (this.groupBy?.period === GROUP_BY_FILTER[2].period) {
|
||||||
|
let week_date = new Date(fromUnixTime(element.timestamp));
|
||||||
|
const first_day = week_date.getDate() - week_date.getDay();
|
||||||
|
const last_day = first_day + 6;
|
||||||
|
const week_first_date = new Date(week_date.setDate(first_day));
|
||||||
|
const week_last_date = new Date(week_date.setDate(last_day));
|
||||||
|
return `${format(week_first_date, 'dd-MMM')} - ${format(
|
||||||
|
week_last_date,
|
||||||
|
'dd-MMM'
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
if (this.groupBy?.period === GROUP_BY_FILTER[3].period) {
|
||||||
|
return format(fromUnixTime(element.timestamp), 'MMM-yyyy');
|
||||||
|
}
|
||||||
|
if (this.groupBy?.period === GROUP_BY_FILTER[4].period) {
|
||||||
|
return format(fromUnixTime(element.timestamp), 'yyyy');
|
||||||
|
}
|
||||||
|
return format(fromUnixTime(element.timestamp), 'dd-MMM');
|
||||||
|
});
|
||||||
|
const datasets = METRIC_CHART[metric.KEY].datasets.map(dataset => {
|
||||||
|
switch (dataset.type) {
|
||||||
|
case 'bar':
|
||||||
|
return {
|
||||||
|
...dataset,
|
||||||
|
yAxisID: 'y-left',
|
||||||
|
label: metric.NAME,
|
||||||
|
data: data.map(element => element.value),
|
||||||
|
};
|
||||||
|
case 'line':
|
||||||
|
return {
|
||||||
|
...dataset,
|
||||||
|
yAxisID: 'y-right',
|
||||||
|
label: this.metrics[0].NAME,
|
||||||
|
data: data.map(element => element.count),
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return dataset;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
labels,
|
||||||
|
datasets,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getChartOptions(metric) {
|
||||||
|
let tooltips = {};
|
||||||
|
if (this.isAverageMetricType(metric.KEY)) {
|
||||||
|
tooltips.callbacks = {
|
||||||
|
label: tooltipItem => {
|
||||||
|
return this.$t(metric.TOOLTIP_TEXT, {
|
||||||
|
metricValue: formatTime(tooltipItem.yLabel),
|
||||||
|
conversationCount: this.accountReport.data[metric.KEY][
|
||||||
|
tooltipItem.index
|
||||||
|
].count,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
scales: METRIC_CHART[metric.KEY].scales,
|
||||||
|
tooltips: tooltips,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<span class="text-sm">{{ metric.NAME }}</span>
|
||||||
|
<div class="flex items-end">
|
||||||
|
<div class="font-medium text-xl">
|
||||||
|
{{ displayMetric(metric.KEY) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="metric.trend" class="text-xs ml-4 flex items-center mb-0.5">
|
||||||
|
<div
|
||||||
|
v-if="metric.trend < 0"
|
||||||
|
class="h-0 w-0 border-x-4 medium border-x-transparent border-t-[8px] mr-1 "
|
||||||
|
:class="trendColor(metric.trend, metric.KEY)"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="h-0 w-0 border-x-4 medium border-x-transparent border-b-[8px] mr-1 "
|
||||||
|
:class="trendColor(metric.trend, metric.KEY)"
|
||||||
|
/>
|
||||||
|
<span class="font-medium" :class="trendColor(metric.trend, metric.KEY)">
|
||||||
|
{{ calculateTrend(metric.KEY) }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import reportMixin from 'dashboard/mixins/reportMixin';
|
||||||
|
export default {
|
||||||
|
mixins: [reportMixin],
|
||||||
|
props: {
|
||||||
|
metric: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
trendColor(value, key) {
|
||||||
|
if (this.isAverageMetricType(key)) {
|
||||||
|
return value > 0
|
||||||
|
? 'border-red-500 text-red-500'
|
||||||
|
: 'border-green-500 text-green-500';
|
||||||
|
}
|
||||||
|
return value < 0
|
||||||
|
? 'border-red-500 text-red-500'
|
||||||
|
: 'border-green-500 text-green-500';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,43 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="filter-container">
|
<div class="flex flex-col md:flex-row justify-between mb-4">
|
||||||
<reports-filters-date-range @on-range-change="onDateRangeChange" />
|
<div class="md:grid flex flex-col filter-container gap-3 w-full">
|
||||||
<woot-date-range-picker
|
<reports-filters-date-range @on-range-change="onDateRangeChange" />
|
||||||
v-if="isDateRangeSelected"
|
<woot-date-range-picker
|
||||||
show-range
|
v-if="isDateRangeSelected"
|
||||||
class="no-margin auto-width"
|
show-range
|
||||||
:value="customDateRange"
|
class="no-margin auto-width"
|
||||||
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
|
:value="customDateRange"
|
||||||
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
|
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
|
||||||
@change="onCustomDateRangeChange"
|
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
|
||||||
/>
|
@change="onCustomDateRangeChange"
|
||||||
<reports-filters-date-group-by
|
/>
|
||||||
v-if="showGroupByFilter && isGroupByPossible"
|
<reports-filters-date-group-by
|
||||||
:valid-group-options="validGroupOptions"
|
v-if="showGroupByFilter && isGroupByPossible"
|
||||||
:selected-option="selectedGroupByFilter"
|
:valid-group-options="validGroupOptions"
|
||||||
@on-grouping-change="onGroupingChange"
|
:selected-option="selectedGroupByFilter"
|
||||||
/>
|
@on-grouping-change="onGroupingChange"
|
||||||
<reports-filters-agents
|
/>
|
||||||
v-if="showAgentsFilter"
|
<reports-filters-agents
|
||||||
@agents-filter-selection="handleAgentsFilterSelection"
|
v-if="showAgentsFilter"
|
||||||
/>
|
@agents-filter-selection="handleAgentsFilterSelection"
|
||||||
<reports-filters-labels
|
/>
|
||||||
v-if="showLabelsFilter"
|
<reports-filters-labels
|
||||||
@labels-filter-selection="handleLabelsFilterSelection"
|
v-if="showLabelsFilter"
|
||||||
/>
|
@labels-filter-selection="handleLabelsFilterSelection"
|
||||||
<reports-filters-teams
|
/>
|
||||||
v-if="showTeamFilter"
|
<reports-filters-teams
|
||||||
@team-filter-selection="handleTeamFilterSelection"
|
v-if="showTeamFilter"
|
||||||
/>
|
@team-filter-selection="handleTeamFilterSelection"
|
||||||
<reports-filters-inboxes
|
/>
|
||||||
v-if="showInboxFilter"
|
<reports-filters-inboxes
|
||||||
@inbox-filter-selection="handleInboxFilterSelection"
|
v-if="showInboxFilter"
|
||||||
/>
|
@inbox-filter-selection="handleInboxFilterSelection"
|
||||||
<reports-filters-ratings
|
/>
|
||||||
v-if="showRatingFilter"
|
<reports-filters-ratings
|
||||||
@rating-filter-selection="handleRatingFilterSelection"
|
v-if="showRatingFilter"
|
||||||
/>
|
@rating-filter-selection="handleRatingFilterSelection"
|
||||||
<div v-if="showBusinessHoursSwitch" class="business-hours">
|
/>
|
||||||
<span class="business-hours-text ">
|
</div>
|
||||||
|
<div v-if="showBusinessHoursSwitch" class="flex items-center">
|
||||||
|
<span class="text-sm whitespace-nowrap mx-2">
|
||||||
{{ $t('REPORT.BUSINESS_HOURS') }}
|
{{ $t('REPORT.BUSINESS_HOURS') }}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
@@ -230,10 +232,6 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.filter-container {
|
.filter-container {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
grid-gap: var(--space-slab);
|
|
||||||
|
|
||||||
margin-bottom: var(--space-normal);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,176 +1,178 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex-container flex-dir-column medium-flex-dir-row">
|
<div class="flex flex-col md:flex-row">
|
||||||
<div
|
<div class="flex items-center w-full flex-col md:flex-row">
|
||||||
v-if="type === 'agent'"
|
<div
|
||||||
class="small-12 medium-3 pull-right multiselect-wrap--small"
|
v-if="type === 'agent'"
|
||||||
>
|
class="md:w-[240px] w-full multiselect-wrap--small"
|
||||||
<p>
|
|
||||||
{{ $t('AGENT_REPORTS.FILTER_DROPDOWN_LABEL') }}
|
|
||||||
</p>
|
|
||||||
<multiselect
|
|
||||||
v-model="currentSelectedFilter"
|
|
||||||
:placeholder="multiselectLabel"
|
|
||||||
label="name"
|
|
||||||
track-by="id"
|
|
||||||
:options="filterItemsList"
|
|
||||||
:option-height="24"
|
|
||||||
:show-labels="false"
|
|
||||||
@input="changeFilterSelection"
|
|
||||||
>
|
>
|
||||||
<template slot="singleLabel" slot-scope="props">
|
<p class="text-xs mb-2 font-medium">
|
||||||
<div class="reports-option__wrap">
|
{{ $t('AGENT_REPORTS.FILTER_DROPDOWN_LABEL') }}
|
||||||
<thumbnail
|
</p>
|
||||||
:src="props.option.thumbnail"
|
<multiselect
|
||||||
:status="props.option.availability_status"
|
v-model="currentSelectedFilter"
|
||||||
:username="props.option.name"
|
:placeholder="multiselectLabel"
|
||||||
size="22px"
|
label="name"
|
||||||
/>
|
track-by="id"
|
||||||
<span class="reports-option__desc">
|
:options="filterItemsList"
|
||||||
<span class="reports-option__title">{{ props.option.name }}</span>
|
:option-height="24"
|
||||||
</span>
|
:show-labels="false"
|
||||||
</div>
|
@input="changeFilterSelection"
|
||||||
</template>
|
>
|
||||||
<template slot="option" slot-scope="props">
|
<template slot="singleLabel" slot-scope="props">
|
||||||
<div class="reports-option__wrap">
|
<div class="reports-option__wrap">
|
||||||
<thumbnail
|
<thumbnail
|
||||||
:src="props.option.thumbnail"
|
:src="props.option.thumbnail"
|
||||||
:status="props.option.availability_status"
|
:status="props.option.availability_status"
|
||||||
:username="props.option.name"
|
:username="props.option.name"
|
||||||
size="22px"
|
size="22px"
|
||||||
/>
|
/>
|
||||||
<p class="reports-option__title">{{ props.option.name }}</p>
|
<span class="reports-option__desc">
|
||||||
</div>
|
<span class="reports-option__title">{{
|
||||||
</template>
|
props.option.name
|
||||||
</multiselect>
|
}}</span>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="type === 'label'"
|
|
||||||
class="small-12 medium-3 pull-right multiselect-wrap--small"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
{{ $t('LABEL_REPORTS.FILTER_DROPDOWN_LABEL') }}
|
|
||||||
</p>
|
|
||||||
<multiselect
|
|
||||||
v-model="currentSelectedFilter"
|
|
||||||
:placeholder="multiselectLabel"
|
|
||||||
label="title"
|
|
||||||
track-by="id"
|
|
||||||
:options="filterItemsList"
|
|
||||||
:option-height="24"
|
|
||||||
:show-labels="false"
|
|
||||||
@input="changeFilterSelection"
|
|
||||||
>
|
|
||||||
<template slot="singleLabel" slot-scope="props">
|
|
||||||
<div class="reports-option__wrap">
|
|
||||||
<div
|
|
||||||
:style="{ backgroundColor: props.option.color }"
|
|
||||||
class="reports-option__rounded--item"
|
|
||||||
/>
|
|
||||||
<span class="reports-option__desc">
|
|
||||||
<span class="reports-option__title">
|
|
||||||
{{ props.option.title }}
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
<template slot="option" slot-scope="props">
|
||||||
<template slot="option" slot-scope="props">
|
<div class="reports-option__wrap">
|
||||||
<div class="reports-option__wrap">
|
<thumbnail
|
||||||
<div
|
:src="props.option.thumbnail"
|
||||||
:style="{ backgroundColor: props.option.color }"
|
:status="props.option.availability_status"
|
||||||
class="
|
:username="props.option.name"
|
||||||
|
size="22px"
|
||||||
|
/>
|
||||||
|
<p class="reports-option__title">{{ props.option.name }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</multiselect>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="type === 'label'"
|
||||||
|
class="md:w-[240px] w-full multiselect-wrap--small"
|
||||||
|
>
|
||||||
|
<p class="text-xs mb-2 font-medium">
|
||||||
|
{{ $t('LABEL_REPORTS.FILTER_DROPDOWN_LABEL') }}
|
||||||
|
</p>
|
||||||
|
<multiselect
|
||||||
|
v-model="currentSelectedFilter"
|
||||||
|
:placeholder="multiselectLabel"
|
||||||
|
label="title"
|
||||||
|
track-by="id"
|
||||||
|
:options="filterItemsList"
|
||||||
|
:option-height="24"
|
||||||
|
:show-labels="false"
|
||||||
|
@input="changeFilterSelection"
|
||||||
|
>
|
||||||
|
<template slot="singleLabel" slot-scope="props">
|
||||||
|
<div class="reports-option__wrap">
|
||||||
|
<div
|
||||||
|
:style="{ backgroundColor: props.option.color }"
|
||||||
|
class="reports-option__rounded--item"
|
||||||
|
/>
|
||||||
|
<span class="reports-option__desc">
|
||||||
|
<span class="reports-option__title">
|
||||||
|
{{ props.option.title }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template slot="option" slot-scope="props">
|
||||||
|
<div class="reports-option__wrap">
|
||||||
|
<div
|
||||||
|
:style="{ backgroundColor: props.option.color }"
|
||||||
|
class="
|
||||||
reports-option__rounded--item
|
reports-option__rounded--item
|
||||||
reports-option__item
|
reports-option__item
|
||||||
reports-option__label--swatch
|
reports-option__label--swatch
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<span class="reports-option__desc">
|
<span class="reports-option__desc">
|
||||||
<span class="reports-option__title">
|
<span class="reports-option__title">
|
||||||
{{ props.option.title }}
|
{{ props.option.title }}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</multiselect>
|
||||||
</multiselect>
|
</div>
|
||||||
|
<div v-else class="md:w-[240px] w-full multiselect-wrap--small">
|
||||||
|
<p class="text-xs mb-2 font-medium">
|
||||||
|
<template v-if="type === 'inbox'">
|
||||||
|
{{ $t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL') }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="type === 'team'">
|
||||||
|
{{ $t('TEAM_REPORTS.FILTER_DROPDOWN_LABEL') }}
|
||||||
|
</template>
|
||||||
|
<!-- handle default condition because the prop is not limited to the given 4 values -->
|
||||||
|
<template v-else>
|
||||||
|
{{ $t('FORMS.MULTISELECT.SELECT_ONE') }}
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
<multiselect
|
||||||
|
v-model="currentSelectedFilter"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:placeholder="multiselectLabel"
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
deselect-label=""
|
||||||
|
:options="filterItemsList"
|
||||||
|
:searchable="false"
|
||||||
|
:allow-empty="false"
|
||||||
|
@input="changeFilterSelection"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mx-1 md:w-[240px] w-full multiselect-wrap--small">
|
||||||
|
<p class="text-xs mb-2 font-medium">
|
||||||
|
{{ $t('REPORT.DURATION_FILTER_LABEL') }}
|
||||||
|
</p>
|
||||||
|
<multiselect
|
||||||
|
v-model="currentDateRangeSelection"
|
||||||
|
track-by="name"
|
||||||
|
label="name"
|
||||||
|
:placeholder="$t('FORMS.MULTISELECT.SELECT_ONE')"
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
deselect-label=""
|
||||||
|
:options="dateRange"
|
||||||
|
:searchable="false"
|
||||||
|
:allow-empty="false"
|
||||||
|
@select="changeDateSelection"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="isDateRangeSelected" class="">
|
||||||
|
<p class="text-xs mb-2 font-medium">
|
||||||
|
{{ $t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER') }}
|
||||||
|
</p>
|
||||||
|
<woot-date-range-picker
|
||||||
|
show-range
|
||||||
|
:value="customDateRange"
|
||||||
|
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
|
||||||
|
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="notLast7Days"
|
||||||
|
class="mx-1 md:w-[240px] w-full multiselect-wrap--small"
|
||||||
|
>
|
||||||
|
<p class="text-xs mb-2 font-medium">
|
||||||
|
{{ $t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL') }}
|
||||||
|
</p>
|
||||||
|
<multiselect
|
||||||
|
v-model="currentSelectedGroupByFilter"
|
||||||
|
track-by="id"
|
||||||
|
label="groupBy"
|
||||||
|
:placeholder="$t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL')"
|
||||||
|
:options="groupByFilterItemsList"
|
||||||
|
:allow-empty="false"
|
||||||
|
:show-labels="false"
|
||||||
|
@input="changeGroupByFilterSelection"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="small-12 medium-3 pull-right multiselect-wrap--small">
|
<div class="flex items-center my-2">
|
||||||
<p>
|
<span class="text-sm mx-2 whitespace-nowrap">
|
||||||
<template v-if="type === 'inbox'">
|
|
||||||
{{ $t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL') }}
|
|
||||||
</template>
|
|
||||||
<template v-else-if="type === 'team'">
|
|
||||||
{{ $t('TEAM_REPORTS.FILTER_DROPDOWN_LABEL') }}
|
|
||||||
</template>
|
|
||||||
<!-- handle default condition because the prop is not limited to the given 4 values -->
|
|
||||||
<template v-else>
|
|
||||||
{{ $t('FORMS.MULTISELECT.SELECT_ONE') }}
|
|
||||||
</template>
|
|
||||||
</p>
|
|
||||||
<multiselect
|
|
||||||
v-model="currentSelectedFilter"
|
|
||||||
track-by="id"
|
|
||||||
label="name"
|
|
||||||
:placeholder="multiselectLabel"
|
|
||||||
selected-label
|
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
|
||||||
deselect-label=""
|
|
||||||
:options="filterItemsList"
|
|
||||||
:searchable="false"
|
|
||||||
:allow-empty="false"
|
|
||||||
@input="changeFilterSelection"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="small-12 medium-3 pull-right margin-right-1 margin-left-1 multiselect-wrap--small"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
{{ $t('REPORT.DURATION_FILTER_LABEL') }}
|
|
||||||
</p>
|
|
||||||
<multiselect
|
|
||||||
v-model="currentDateRangeSelection"
|
|
||||||
track-by="name"
|
|
||||||
label="name"
|
|
||||||
:placeholder="$t('FORMS.MULTISELECT.SELECT_ONE')"
|
|
||||||
selected-label
|
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
|
||||||
deselect-label=""
|
|
||||||
:options="dateRange"
|
|
||||||
:searchable="false"
|
|
||||||
:allow-empty="false"
|
|
||||||
@select="changeDateSelection"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="isDateRangeSelected" class="">
|
|
||||||
<p>
|
|
||||||
{{ $t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER') }}
|
|
||||||
</p>
|
|
||||||
<woot-date-range-picker
|
|
||||||
show-range
|
|
||||||
:value="customDateRange"
|
|
||||||
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
|
|
||||||
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
|
|
||||||
@change="onChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="notLast7Days"
|
|
||||||
class="small-12 medium-3 pull-right margin-right-1 margin-left-1 multiselect-wrap--small"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
{{ $t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL') }}
|
|
||||||
</p>
|
|
||||||
<multiselect
|
|
||||||
v-model="currentSelectedGroupByFilter"
|
|
||||||
track-by="id"
|
|
||||||
label="groupBy"
|
|
||||||
:placeholder="$t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL')"
|
|
||||||
:options="groupByFilterItemsList"
|
|
||||||
:allow-empty="false"
|
|
||||||
:show-labels="false"
|
|
||||||
@input="changeGroupByFilterSelection"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="small-12 medium-3 business-hours">
|
|
||||||
<span class="business-hours-text">
|
|
||||||
{{ $t('REPORT.BUSINESS_HOURS') }}
|
{{ $t('REPORT.BUSINESS_HOURS') }}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
@@ -266,7 +268,7 @@ export default {
|
|||||||
1: GROUP_BY_FILTER[2].period,
|
1: GROUP_BY_FILTER[2].period,
|
||||||
2: GROUP_BY_FILTER[3].period,
|
2: GROUP_BY_FILTER[3].period,
|
||||||
3: GROUP_BY_FILTER[3].period,
|
3: GROUP_BY_FILTER[3].period,
|
||||||
4: GROUP_BY_FILTER[3].period,
|
4: GROUP_BY_FILTER[4].period,
|
||||||
};
|
};
|
||||||
return groupRange[this.currentDateRangeSelection.id];
|
return groupRange[this.currentDateRangeSelection.id];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,48 +19,15 @@
|
|||||||
@group-by-filter-change="onGroupByFilterChange"
|
@group-by-filter-change="onGroupByFilterChange"
|
||||||
@business-hours-toggle="onBusinessHoursToggle"
|
@business-hours-toggle="onBusinessHoursToggle"
|
||||||
/>
|
/>
|
||||||
<div>
|
<report-container v-if="filterItemsList.length" :group-by="groupBy" />
|
||||||
<div v-if="filterItemsList.length" class="row">
|
|
||||||
<woot-report-stats-card
|
|
||||||
v-for="(metric, index) in metrics"
|
|
||||||
:key="metric.NAME"
|
|
||||||
:desc="metric.DESC"
|
|
||||||
:heading="metric.NAME"
|
|
||||||
:info-text="displayInfoText(metric.KEY)"
|
|
||||||
:index="index"
|
|
||||||
:on-click="changeSelection"
|
|
||||||
:point="displayMetric(metric.KEY)"
|
|
||||||
:trend="calculateTrend(metric.KEY)"
|
|
||||||
:selected="index === currentSelection"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="report-bar">
|
|
||||||
<woot-loading-state
|
|
||||||
v-if="accountReport.isFetching"
|
|
||||||
:message="$t('REPORT.LOADING_CHART')"
|
|
||||||
/>
|
|
||||||
<div v-else class="chart-container">
|
|
||||||
<woot-bar
|
|
||||||
v-if="accountReport.data.length && filterItemsList.length"
|
|
||||||
:collection="collection"
|
|
||||||
:chart-options="chartOptions"
|
|
||||||
/>
|
|
||||||
<span v-else class="empty-state">
|
|
||||||
{{ $t('REPORT.NO_ENOUGH_DATA') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ReportFilters from './ReportFilters';
|
import ReportFilters from './ReportFilters';
|
||||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
import ReportContainer from '../ReportContainer.vue';
|
||||||
import format from 'date-fns/format';
|
import { GROUP_BY_FILTER } from '../constants';
|
||||||
import { GROUP_BY_FILTER, METRIC_CHART } from '../constants';
|
|
||||||
import reportMixin from '../../../../../mixins/reportMixin';
|
import reportMixin from '../../../../../mixins/reportMixin';
|
||||||
import { formatTime } from '@chatwoot/utils';
|
|
||||||
import { generateFileName } from '../../../../../helper/downloadHelper';
|
import { generateFileName } from '../../../../../helper/downloadHelper';
|
||||||
import { REPORTS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
import { REPORTS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
||||||
|
|
||||||
@@ -72,9 +39,11 @@ const REPORTS_KEYS = {
|
|||||||
RESOLUTION_TIME: 'avg_resolution_time',
|
RESOLUTION_TIME: 'avg_resolution_time',
|
||||||
RESOLUTION_COUNT: 'resolutions_count',
|
RESOLUTION_COUNT: 'resolutions_count',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ReportFilters,
|
ReportFilters,
|
||||||
|
ReportContainer,
|
||||||
},
|
},
|
||||||
mixins: [reportMixin],
|
mixins: [reportMixin],
|
||||||
props: {
|
props: {
|
||||||
@@ -99,7 +68,6 @@ export default {
|
|||||||
return {
|
return {
|
||||||
from: 0,
|
from: 0,
|
||||||
to: 0,
|
to: 0,
|
||||||
currentSelection: 0,
|
|
||||||
selectedFilter: null,
|
selectedFilter: null,
|
||||||
groupBy: GROUP_BY_FILTER[1],
|
groupBy: GROUP_BY_FILTER[1],
|
||||||
groupByfilterItemsList: this.$t('REPORT.GROUP_BY_DAY_OPTIONS'),
|
groupByfilterItemsList: this.$t('REPORT.GROUP_BY_DAY_OPTIONS'),
|
||||||
@@ -111,115 +79,6 @@ export default {
|
|||||||
filterItemsList() {
|
filterItemsList() {
|
||||||
return this.$store.getters[this.getterKey] || [];
|
return this.$store.getters[this.getterKey] || [];
|
||||||
},
|
},
|
||||||
accountSummary() {
|
|
||||||
return this.$store.getters.getAccountSummary || [];
|
|
||||||
},
|
|
||||||
accountReport() {
|
|
||||||
return this.$store.getters.getAccountReports || [];
|
|
||||||
},
|
|
||||||
collection() {
|
|
||||||
if (this.accountReport.isFetching) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
if (!this.accountReport.data.length) return {};
|
|
||||||
const labels = this.accountReport.data.map(element => {
|
|
||||||
if (this.groupBy.period === GROUP_BY_FILTER[2].period) {
|
|
||||||
let week_date = new Date(fromUnixTime(element.timestamp));
|
|
||||||
const first_day = week_date.getDate() - week_date.getDay();
|
|
||||||
const last_day = first_day + 6;
|
|
||||||
|
|
||||||
const week_first_date = new Date(week_date.setDate(first_day));
|
|
||||||
const week_last_date = new Date(week_date.setDate(last_day));
|
|
||||||
|
|
||||||
return `${format(week_first_date, 'dd/MM/yy')} - ${format(
|
|
||||||
week_last_date,
|
|
||||||
'dd/MM/yy'
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
if (this.groupBy.period === GROUP_BY_FILTER[3].period) {
|
|
||||||
return format(fromUnixTime(element.timestamp), 'MMM-yyyy');
|
|
||||||
}
|
|
||||||
if (this.groupBy.period === GROUP_BY_FILTER[4].period) {
|
|
||||||
return format(fromUnixTime(element.timestamp), 'yyyy');
|
|
||||||
}
|
|
||||||
return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy');
|
|
||||||
});
|
|
||||||
|
|
||||||
const datasets = METRIC_CHART[
|
|
||||||
this.metrics[this.currentSelection].KEY
|
|
||||||
].datasets.map(dataset => {
|
|
||||||
switch (dataset.type) {
|
|
||||||
case 'bar':
|
|
||||||
return {
|
|
||||||
...dataset,
|
|
||||||
yAxisID: 'y-left',
|
|
||||||
label: this.metrics[this.currentSelection].NAME,
|
|
||||||
data: this.accountReport.data.map(element => element.value),
|
|
||||||
};
|
|
||||||
case 'line':
|
|
||||||
return {
|
|
||||||
...dataset,
|
|
||||||
yAxisID: 'y-right',
|
|
||||||
label: this.metrics[0].NAME,
|
|
||||||
data: this.accountReport.data.map(element => element.count),
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return dataset;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels,
|
|
||||||
datasets,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
chartOptions() {
|
|
||||||
let tooltips = {};
|
|
||||||
if (this.isAverageMetricType(this.metrics[this.currentSelection].KEY)) {
|
|
||||||
tooltips.callbacks = {
|
|
||||||
label: tooltipItem => {
|
|
||||||
return this.$t(this.metrics[this.currentSelection].TOOLTIP_TEXT, {
|
|
||||||
metricValue: formatTime(tooltipItem.yLabel),
|
|
||||||
conversationCount: this.accountReport.data[tooltipItem.index]
|
|
||||||
.count,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
scales: METRIC_CHART[this.metrics[this.currentSelection].KEY].scales,
|
|
||||||
tooltips: tooltips,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
metrics() {
|
|
||||||
let reportKeys = ['CONVERSATIONS'];
|
|
||||||
// If report type is agent, we don't need to show
|
|
||||||
// incoming messages count, as there will not be any message
|
|
||||||
// sent by an agent which is incoming.
|
|
||||||
if (this.type !== 'agent') {
|
|
||||||
reportKeys.push('INCOMING_MESSAGES');
|
|
||||||
}
|
|
||||||
reportKeys = [
|
|
||||||
...reportKeys,
|
|
||||||
'OUTGOING_MESSAGES',
|
|
||||||
'FIRST_RESPONSE_TIME',
|
|
||||||
'RESOLUTION_TIME',
|
|
||||||
'RESOLUTION_COUNT',
|
|
||||||
];
|
|
||||||
const infoText = {
|
|
||||||
FIRST_RESPONSE_TIME: this.$t(
|
|
||||||
`REPORT.METRICS.FIRST_RESPONSE_TIME.INFO_TEXT`
|
|
||||||
),
|
|
||||||
RESOLUTION_TIME: this.$t(`REPORT.METRICS.RESOLUTION_TIME.INFO_TEXT`),
|
|
||||||
};
|
|
||||||
return reportKeys.map(key => ({
|
|
||||||
NAME: this.$t(`REPORT.METRICS.${key}.NAME`),
|
|
||||||
KEY: REPORTS_KEYS[key],
|
|
||||||
DESC: this.$t(`REPORT.METRICS.${key}.DESC`),
|
|
||||||
INFO_TEXT: infoText[key],
|
|
||||||
TOOLTIP_TEXT: `REPORT.METRICS.${key}.TOOLTIP_TEXT`,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch(this.actionKey);
|
this.$store.dispatch(this.actionKey);
|
||||||
@@ -240,15 +99,28 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchChartData() {
|
fetchChartData() {
|
||||||
const { from, to, groupBy, businessHours } = this;
|
[
|
||||||
this.$store.dispatch('fetchAccountReport', {
|
'CONVERSATIONS',
|
||||||
metric: this.metrics[this.currentSelection].KEY,
|
'INCOMING_MESSAGES',
|
||||||
from,
|
'OUTGOING_MESSAGES',
|
||||||
to,
|
'FIRST_RESPONSE_TIME',
|
||||||
type: this.type,
|
'RESOLUTION_TIME',
|
||||||
id: this.selectedFilter.id,
|
'RESOLUTION_COUNT',
|
||||||
groupBy: groupBy.period,
|
].forEach(async key => {
|
||||||
businessHours,
|
try {
|
||||||
|
const { from, to, groupBy, businessHours } = this;
|
||||||
|
this.$store.dispatch('fetchAccountReport', {
|
||||||
|
metric: REPORTS_KEYS[key],
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
type: this.type,
|
||||||
|
id: this.selectedFilter.id,
|
||||||
|
groupBy: groupBy.period,
|
||||||
|
businessHours,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
this.showAlert(this.$t('REPORT.DATA_FETCHING_FAILED'));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
downloadReports() {
|
downloadReports() {
|
||||||
@@ -265,10 +137,6 @@ export default {
|
|||||||
this.$store.dispatch(dispatchMethods[type], params);
|
this.$store.dispatch(dispatchMethods[type], params);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
changeSelection(index) {
|
|
||||||
this.currentSelection = index;
|
|
||||||
this.fetchChartData();
|
|
||||||
},
|
|
||||||
onDateRangeChange({ from, to, groupBy }) {
|
onDateRangeChange({ from, to, groupBy }) {
|
||||||
// do not track filter change on inital load
|
// do not track filter change on inital load
|
||||||
if (this.from !== 0 && this.to !== 0) {
|
if (this.from !== 0 && this.to !== 0) {
|
||||||
|
|||||||
@@ -1,4 +1,25 @@
|
|||||||
import { formatTime } from '@chatwoot/utils';
|
export const formatTime = timeInSeconds => {
|
||||||
|
if (!timeInSeconds) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeInSeconds < 60) {
|
||||||
|
return `${timeInSeconds}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeInSeconds < 3600) {
|
||||||
|
const minutes = Math.floor(timeInSeconds / 60);
|
||||||
|
return `${minutes}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeInSeconds < 86400) {
|
||||||
|
const hours = Math.floor(timeInSeconds / 3600);
|
||||||
|
return `${hours}h`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = Math.floor(timeInSeconds / 86400);
|
||||||
|
return `${days}d`;
|
||||||
|
};
|
||||||
|
|
||||||
export const GROUP_BY_FILTER = {
|
export const GROUP_BY_FILTER = {
|
||||||
1: { id: 1, period: 'day' },
|
1: { id: 1, period: 'day' },
|
||||||
@@ -57,21 +78,13 @@ export const DATE_RANGE_OPTIONS = {
|
|||||||
id: 'LAST_6_MONTHS',
|
id: 'LAST_6_MONTHS',
|
||||||
translationKey: 'REPORT.DATE_RANGE_OPTIONS.LAST_6_MONTHS',
|
translationKey: 'REPORT.DATE_RANGE_OPTIONS.LAST_6_MONTHS',
|
||||||
offset: 179,
|
offset: 179,
|
||||||
groupByOptions: [
|
groupByOptions: [GROUP_BY_OPTIONS.WEEK, GROUP_BY_OPTIONS.MONTH],
|
||||||
GROUP_BY_OPTIONS.DAY,
|
|
||||||
GROUP_BY_OPTIONS.WEEK,
|
|
||||||
GROUP_BY_OPTIONS.MONTH,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
LAST_YEAR: {
|
LAST_YEAR: {
|
||||||
id: 'LAST_YEAR',
|
id: 'LAST_YEAR',
|
||||||
translationKey: 'REPORT.DATE_RANGE_OPTIONS.LAST_YEAR',
|
translationKey: 'REPORT.DATE_RANGE_OPTIONS.LAST_YEAR',
|
||||||
offset: 364,
|
offset: 364,
|
||||||
groupByOptions: [
|
groupByOptions: [GROUP_BY_OPTIONS.WEEK, GROUP_BY_OPTIONS.MONTH],
|
||||||
GROUP_BY_OPTIONS.DAY,
|
|
||||||
GROUP_BY_OPTIONS.WEEK,
|
|
||||||
GROUP_BY_OPTIONS.MONTH,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
CUSTOM_DATE_RANGE: {
|
CUSTOM_DATE_RANGE: {
|
||||||
id: 'CUSTOM_DATE_RANGE',
|
id: 'CUSTOM_DATE_RANGE',
|
||||||
@@ -87,7 +100,7 @@ export const DATE_RANGE_OPTIONS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CHART_FONT_FAMILY =
|
export const CHART_FONT_FAMILY =
|
||||||
'-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
'PlusJakarta,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
||||||
|
|
||||||
export const DEFAULT_LINE_CHART = {
|
export const DEFAULT_LINE_CHART = {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
@@ -123,6 +136,12 @@ export const DEFAULT_CHART = {
|
|||||||
fontFamily: CHART_FONT_FAMILY,
|
fontFamily: CHART_FONT_FAMILY,
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
stepSize: 1,
|
stepSize: 1,
|
||||||
|
callback: (value, index, values) => {
|
||||||
|
if (!index || index === values.length - 1) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
gridLines: {
|
gridLines: {
|
||||||
drawOnChartArea: false,
|
drawOnChartArea: false,
|
||||||
@@ -156,8 +175,11 @@ export const METRIC_CHART = {
|
|||||||
position: 'left',
|
position: 'left',
|
||||||
ticks: {
|
ticks: {
|
||||||
fontFamily: CHART_FONT_FAMILY,
|
fontFamily: CHART_FONT_FAMILY,
|
||||||
callback(value) {
|
callback: (value, index, values) => {
|
||||||
return formatTime(value);
|
if (!index || index === values.length - 1) {
|
||||||
|
return formatTime(value);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
gridLines: {
|
gridLines: {
|
||||||
@@ -187,8 +209,11 @@ export const METRIC_CHART = {
|
|||||||
position: 'left',
|
position: 'left',
|
||||||
ticks: {
|
ticks: {
|
||||||
fontFamily: CHART_FONT_FAMILY,
|
fontFamily: CHART_FONT_FAMILY,
|
||||||
callback(value) {
|
callback: (value, index, values) => {
|
||||||
return formatTime(value);
|
if (!index || index === values.length - 1) {
|
||||||
|
return formatTime(value);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
gridLines: {
|
gridLines: {
|
||||||
|
|||||||
@@ -11,10 +11,23 @@ import {
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
fetchingStatus: false,
|
fetchingStatus: false,
|
||||||
reportData: [],
|
|
||||||
accountReport: {
|
accountReport: {
|
||||||
isFetching: false,
|
isFetching: {
|
||||||
data: [],
|
conversations_count: false,
|
||||||
|
incoming_messages_count: false,
|
||||||
|
outgoing_messages_count: false,
|
||||||
|
avg_first_response_time: false,
|
||||||
|
avg_resolution_time: false,
|
||||||
|
resolutions_count: false,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
conversations_count: [],
|
||||||
|
incoming_messages_count: [],
|
||||||
|
outgoing_messages_count: [],
|
||||||
|
avg_first_response_time: [],
|
||||||
|
avg_resolution_time: [],
|
||||||
|
resolutions_count: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
accountSummary: {
|
accountSummary: {
|
||||||
avg_first_response_time: 0,
|
avg_first_response_time: 0,
|
||||||
@@ -60,12 +73,22 @@ const getters = {
|
|||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
fetchAccountReport({ commit }, reportObj) {
|
fetchAccountReport({ commit }, reportObj) {
|
||||||
commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, true);
|
const { metric } = reportObj;
|
||||||
|
commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, {
|
||||||
|
metric,
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
Report.getReports(reportObj).then(accountReport => {
|
Report.getReports(reportObj).then(accountReport => {
|
||||||
let { data } = accountReport;
|
let { data } = accountReport;
|
||||||
data = clampDataBetweenTimeline(data, reportObj.from, reportObj.to);
|
data = clampDataBetweenTimeline(data, reportObj.from, reportObj.to);
|
||||||
commit(types.default.SET_ACCOUNT_REPORTS, data);
|
commit(types.default.SET_ACCOUNT_REPORTS, {
|
||||||
commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, false);
|
metric,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, {
|
||||||
|
metric,
|
||||||
|
value: false,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchAccountConversationHeatmap({ commit }, reportObj) {
|
fetchAccountConversationHeatmap({ commit }, reportObj) {
|
||||||
@@ -202,14 +225,14 @@ export const actions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mutations = {
|
const mutations = {
|
||||||
[types.default.SET_ACCOUNT_REPORTS](_state, accountReport) {
|
[types.default.SET_ACCOUNT_REPORTS](_state, { metric, data }) {
|
||||||
_state.accountReport.data = accountReport;
|
_state.accountReport.data[metric] = data;
|
||||||
},
|
},
|
||||||
[types.default.SET_HEATMAP_DATA](_state, heatmapData) {
|
[types.default.SET_HEATMAP_DATA](_state, heatmapData) {
|
||||||
_state.overview.accountConversationHeatmap = heatmapData;
|
_state.overview.accountConversationHeatmap = heatmapData;
|
||||||
},
|
},
|
||||||
[types.default.TOGGLE_ACCOUNT_REPORT_LOADING](_state, flag) {
|
[types.default.TOGGLE_ACCOUNT_REPORT_LOADING](_state, { metric, value }) {
|
||||||
_state.accountReport.isFetching = flag;
|
_state.accountReport.isFetching[metric] = value;
|
||||||
},
|
},
|
||||||
[types.default.TOGGLE_HEATMAP_LOADING](_state, flag) {
|
[types.default.TOGGLE_HEATMAP_LOADING](_state, flag) {
|
||||||
_state.overview.uiFlags.isFetchingAccountConversationsHeatmap = flag;
|
_state.overview.uiFlags.isFetchingAccountConversationsHeatmap = flag;
|
||||||
|
|||||||
Reference in New Issue
Block a user