feat: Add date-range filter on CSAT Reports (#2622)
This commit is contained in:
parent
a5bc81b304
commit
3d18ec9e40
10 changed files with 175 additions and 120 deletions
|
@ -1,4 +1,5 @@
|
|||
class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::BaseController
|
||||
include Sift
|
||||
include DateRangeHelper
|
||||
|
||||
RESULTS_PER_PAGE = 25
|
||||
|
@ -9,6 +10,8 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
|||
before_action :set_current_page_surveys, only: [:index]
|
||||
before_action :set_total_sent_messages_count, only: [:metrics]
|
||||
|
||||
sort_on :created_at, type: :datetime
|
||||
|
||||
def index; end
|
||||
|
||||
def metrics
|
||||
|
@ -25,7 +28,9 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
|||
end
|
||||
|
||||
def set_csat_survey_responses
|
||||
@csat_survey_responses = Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
||||
@csat_survey_responses = filtrate(
|
||||
Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
||||
)
|
||||
@csat_survey_responses = @csat_survey_responses.where(created_at: range) if range.present?
|
||||
end
|
||||
|
||||
|
|
|
@ -6,12 +6,16 @@ class CSATReportsAPI extends ApiClient {
|
|||
super('csat_survey_responses', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ page } = {}) {
|
||||
return axios.get(this.url, { params: { page } });
|
||||
get({ page, from, to } = {}) {
|
||||
return axios.get(this.url, {
|
||||
params: { page, since: from, until: to, sort: '-created_at' },
|
||||
});
|
||||
}
|
||||
|
||||
getMetrics() {
|
||||
return axios.get(`${this.url}/metrics`);
|
||||
getMetrics({ from, to } = {}) {
|
||||
return axios.get(`${this.url}/metrics`, {
|
||||
params: { since: from, until: to },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,18 +11,26 @@ describe('#Reports API', () => {
|
|||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#get', () => {
|
||||
csatReportsAPI.get({ page: 1 });
|
||||
csatReportsAPI.get({ page: 1, from: 1622485800, to: 1623695400 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/csat_survey_responses',
|
||||
{
|
||||
params: { page: 1 },
|
||||
params: {
|
||||
page: 1,
|
||||
since: 1622485800,
|
||||
until: 1623695400,
|
||||
sort: '-created_at',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
it('#getMetrics', () => {
|
||||
csatReportsAPI.getMetrics();
|
||||
csatReportsAPI.getMetrics({ from: 1622485800, to: 1623695400 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/csat_survey_responses/metrics'
|
||||
'/api/v1/csat_survey_responses/metrics',
|
||||
{
|
||||
params: { since: 1622485800, until: 1623695400 },
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div class="column content-box">
|
||||
<report-date-range-selector @date-range-change="onDateRangeChange" />
|
||||
<csat-metrics />
|
||||
<csat-table :page-index="pageIndex" @page-change="onPageNumberChange" />
|
||||
</div>
|
||||
|
@ -7,26 +8,38 @@
|
|||
<script>
|
||||
import CsatMetrics from './components/CsatMetrics';
|
||||
import CsatTable from './components/CsatTable';
|
||||
import ReportDateRangeSelector from './components/DateRangeSelector';
|
||||
|
||||
export default {
|
||||
name: 'CsatResponses',
|
||||
components: {
|
||||
CsatMetrics,
|
||||
CsatTable,
|
||||
ReportDateRangeSelector,
|
||||
},
|
||||
data() {
|
||||
return { pageIndex: 1 };
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('csat/getMetrics');
|
||||
this.getData();
|
||||
return { pageIndex: 1, from: 0, to: 0 };
|
||||
},
|
||||
methods: {
|
||||
getData() {
|
||||
this.$store.dispatch('csat/get', { page: this.pageIndex });
|
||||
getAllData() {
|
||||
this.$store.dispatch('csat/getMetrics', { from: this.from, to: this.to });
|
||||
this.getResponses();
|
||||
},
|
||||
getResponses() {
|
||||
this.$store.dispatch('csat/get', {
|
||||
page: this.pageIndex,
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
});
|
||||
},
|
||||
onPageNumberChange(pageIndex) {
|
||||
this.pageIndex = pageIndex;
|
||||
this.getData();
|
||||
this.getResponses();
|
||||
},
|
||||
onDateRangeChange({ from, to }) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.getAllData();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -8,30 +8,7 @@
|
|||
>
|
||||
{{ $t('REPORT.DOWNLOAD_AGENT_REPORTS') }}
|
||||
</woot-button>
|
||||
<div class="range-selector">
|
||||
<div class="small-3 pull-right">
|
||||
<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>
|
||||
<woot-date-range-picker
|
||||
v-if="isDateRangeSelected"
|
||||
:value="customDateRange"
|
||||
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
|
||||
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
<report-date-range-selector @date-range-change="onDateRangeChange" />
|
||||
<div class="row">
|
||||
<woot-report-stats-card
|
||||
v-for="(metric, index) in metrics"
|
||||
|
@ -61,12 +38,9 @@
|
|||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import startOfDay from 'date-fns/startOfDay';
|
||||
import subDays from 'date-fns/subDays';
|
||||
import getUnixTime from 'date-fns/getUnixTime';
|
||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
import format from 'date-fns/format';
|
||||
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
||||
import ReportDateRangeSelector from './components/DateRangeSelector';
|
||||
|
||||
const REPORTS_KEYS = {
|
||||
CONVERSATIONS: 'conversations_count',
|
||||
|
@ -77,46 +51,18 @@ const REPORTS_KEYS = {
|
|||
RESOLUTION_COUNT: 'resolutions_count',
|
||||
};
|
||||
|
||||
const CUSTOM_DATE_RANGE_ID = 5;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDateRangePicker,
|
||||
ReportDateRangeSelector,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentSelection: 0,
|
||||
currentDateRangeSelection: this.$t('REPORT.DATE_RANGE')[0],
|
||||
dateRange: this.$t('REPORT.DATE_RANGE'),
|
||||
customDateRange: [new Date(), new Date()],
|
||||
};
|
||||
return { from: 0, to: 0, currentSelection: 0 };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountSummary: 'getAccountSummary',
|
||||
accountReport: 'getAccountReports',
|
||||
}),
|
||||
to() {
|
||||
if (this.isDateRangeSelected) {
|
||||
return this.fromCustomDate(this.customDateRange[1]);
|
||||
}
|
||||
return this.fromCustomDate(new Date());
|
||||
},
|
||||
from() {
|
||||
if (this.isDateRangeSelected) {
|
||||
return this.fromCustomDate(this.customDateRange[0]);
|
||||
}
|
||||
const dateRange = {
|
||||
0: 6,
|
||||
1: 29,
|
||||
2: 89,
|
||||
3: 179,
|
||||
4: 364,
|
||||
};
|
||||
const diff = dateRange[this.currentDateRangeSelection.id];
|
||||
const fromDate = subDays(new Date(), diff);
|
||||
return this.fromCustomDate(fromDate);
|
||||
},
|
||||
collection() {
|
||||
if (this.accountReport.isFetching) {
|
||||
return {};
|
||||
|
@ -152,32 +98,11 @@ export default {
|
|||
DESC: this.$t(`REPORT.METRICS.${key}.DESC`),
|
||||
}));
|
||||
},
|
||||
isDateRangeSelected() {
|
||||
return this.currentDateRangeSelection.id === CUSTOM_DATE_RANGE_ID;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchAllData();
|
||||
},
|
||||
methods: {
|
||||
fetchAllData() {
|
||||
const { from, to } = this;
|
||||
this.$store.dispatch('fetchAccountSummary', {
|
||||
from,
|
||||
to,
|
||||
});
|
||||
this.$store.dispatch('fetchAccountReport', {
|
||||
metric: this.metrics[this.currentSelection].KEY,
|
||||
from,
|
||||
to,
|
||||
});
|
||||
},
|
||||
changeDateSelection(selectedRange) {
|
||||
this.currentDateRangeSelection = selectedRange;
|
||||
this.fetchAllData();
|
||||
},
|
||||
changeSelection(index) {
|
||||
this.currentSelection = index;
|
||||
this.$store.dispatch('fetchAccountSummary', { from, to });
|
||||
this.fetchChartData();
|
||||
},
|
||||
fetchChartData() {
|
||||
|
@ -190,23 +115,17 @@ export default {
|
|||
},
|
||||
downloadAgentReports() {
|
||||
const { from, to } = this;
|
||||
this.$store.dispatch('downloadAgentReports', {
|
||||
from,
|
||||
to,
|
||||
});
|
||||
this.$store.dispatch('downloadAgentReports', { from, to });
|
||||
},
|
||||
fromCustomDate(date) {
|
||||
return getUnixTime(startOfDay(date));
|
||||
changeSelection(index) {
|
||||
this.currentSelection = index;
|
||||
this.fetchChartData();
|
||||
},
|
||||
onChange(value) {
|
||||
this.customDateRange = value;
|
||||
onDateRangeChange({ from, to }) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.fetchAllData();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.range-selector {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="csat--table-container">
|
||||
<ve-table
|
||||
max-height="calc(100vh - 30rem)"
|
||||
max-height="calc(100vh - 35rem)"
|
||||
:fixed-header="true"
|
||||
:columns="columns"
|
||||
:table-data="tableData"
|
||||
|
@ -25,12 +25,14 @@ import { VeTable, VePagination } from 'vue-easytable';
|
|||
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName';
|
||||
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||
import { mapGetters } from 'vuex';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VeTable,
|
||||
VePagination,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
pageIndex: {
|
||||
type: Number,
|
||||
|
@ -93,6 +95,7 @@ export default {
|
|||
key: 'feedbackText',
|
||||
title: this.$t('CSAT_REPORTS.TABLE.HEADER.FEEDBACK_TEXT'),
|
||||
align: 'left',
|
||||
width: 400,
|
||||
},
|
||||
{
|
||||
field: 'converstionId',
|
||||
|
@ -106,9 +109,14 @@ export default {
|
|||
params: { conversation_id: row.conversationId },
|
||||
};
|
||||
return (
|
||||
<router-link to={routerParams}>
|
||||
{`#${row.conversationId}`}
|
||||
</router-link>
|
||||
<div class="text-right">
|
||||
<router-link to={routerParams}>
|
||||
{`#${row.conversationId}`}
|
||||
</router-link>
|
||||
<div class="csat--timestamp" v-tooltip={row.createdAt}>
|
||||
{row.createdAgo}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
@ -121,6 +129,8 @@ export default {
|
|||
rating: response.rating,
|
||||
feedbackText: response.feedback_message || '---',
|
||||
conversationId: response.conversation_id,
|
||||
createdAgo: this.dynamicTime(response.created_at),
|
||||
createdAt: this.messageStamp(response.created_at, 'LLL d yyyy, h:mm a'),
|
||||
}));
|
||||
},
|
||||
},
|
||||
|
@ -188,4 +198,9 @@ export default {
|
|||
margin-top: -1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.csat--timestamp {
|
||||
color: var(--b-400);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<div class="flex-container flex-dir-column medium-flex-dir-row">
|
||||
<div class="small-12 medium-3 pull-right">
|
||||
<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>
|
||||
<woot-date-range-picker
|
||||
v-if="isDateRangeSelected"
|
||||
:value="customDateRange"
|
||||
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
|
||||
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
||||
const CUSTOM_DATE_RANGE_ID = 5;
|
||||
import subDays from 'date-fns/subDays';
|
||||
import startOfDay from 'date-fns/startOfDay';
|
||||
import getUnixTime from 'date-fns/getUnixTime';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDateRangePicker,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentDateRangeSelection: this.$t('REPORT.DATE_RANGE')[0],
|
||||
dateRange: this.$t('REPORT.DATE_RANGE'),
|
||||
customDateRange: [new Date(), new Date()],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDateRangeSelected() {
|
||||
return this.currentDateRangeSelection.id === CUSTOM_DATE_RANGE_ID;
|
||||
},
|
||||
to() {
|
||||
if (this.isDateRangeSelected) {
|
||||
return this.fromCustomDate(this.customDateRange[1]);
|
||||
}
|
||||
return this.fromCustomDate(new Date());
|
||||
},
|
||||
from() {
|
||||
if (this.isDateRangeSelected) {
|
||||
return this.fromCustomDate(this.customDateRange[0]);
|
||||
}
|
||||
const dateRange = {
|
||||
0: 6,
|
||||
1: 29,
|
||||
2: 89,
|
||||
3: 179,
|
||||
4: 364,
|
||||
};
|
||||
const diff = dateRange[this.currentDateRangeSelection.id];
|
||||
const fromDate = subDays(new Date(), diff);
|
||||
return this.fromCustomDate(fromDate);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.onDateRangeChange();
|
||||
},
|
||||
methods: {
|
||||
onDateRangeChange() {
|
||||
this.$emit('date-range-change', { from: this.from, to: this.to });
|
||||
},
|
||||
fromCustomDate(date) {
|
||||
return getUnixTime(startOfDay(date));
|
||||
},
|
||||
changeDateSelection(selectedRange) {
|
||||
this.currentDateRangeSelection = selectedRange;
|
||||
this.onDateRangeChange();
|
||||
},
|
||||
onChange(value) {
|
||||
this.customDateRange = value;
|
||||
this.onDateRangeChange();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -82,10 +82,10 @@ export const getters = {
|
|||
};
|
||||
|
||||
export const actions = {
|
||||
get: async function getResponses({ commit }, { page = 1 } = {}) {
|
||||
get: async function getResponses({ commit }, { page = 1, from, to } = {}) {
|
||||
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const response = await CSATReports.get({ page });
|
||||
const response = await CSATReports.get({ page, from, to });
|
||||
commit(types.SET_CSAT_RESPONSE, response.data);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
|
@ -93,10 +93,10 @@ export const actions = {
|
|||
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
getMetrics: async function getMetrics({ commit }) {
|
||||
getMetrics: async function getMetrics({ commit }, { from, to }) {
|
||||
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: true });
|
||||
try {
|
||||
const response = await CSATReports.getMetrics();
|
||||
const response = await CSATReports.getMetrics({ from, to });
|
||||
commit(types.SET_CSAT_RESPONSE_METRICS, response.data);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
|
|
|
@ -101,7 +101,7 @@ export default {
|
|||
},
|
||||
readableTime() {
|
||||
const { created_at: createdAt = '' } = this.message;
|
||||
return this.messageStamp(createdAt);
|
||||
return this.messageStamp(createdAt, 'LLL d yyyy, h:mm a');
|
||||
},
|
||||
messageType() {
|
||||
const { message_type: type = 1 } = this.message;
|
||||
|
|
|
@ -14,4 +14,4 @@ if resource.assigned_agent
|
|||
json.partial! 'api/v1/models/agent.json.jbuilder', resource: resource.assigned_agent
|
||||
end
|
||||
end
|
||||
json.created_at resource.created_at
|
||||
json.created_at resource.created_at.to_i
|
||||
|
|
Loading…
Reference in a new issue