feat: Add date-range filter on CSAT Reports (#2622)

This commit is contained in:
Pranav Raj S 2021-07-14 20:15:57 +05:30 committed by GitHub
parent a5bc81b304
commit 3d18ec9e40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 175 additions and 120 deletions

View file

@ -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

View file

@ -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 },
});
}
}

View file

@ -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 },
}
);
});
});

View file

@ -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();
},
},
};

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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;

View file

@ -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