fix: CSAT filter metrics rendering & conversation reports not working [CW-1840, CW-1818] (#7170)

* fix: emoji rendering for CSAT

* feat: add tests for CSAT Metrics

* fix: allow rating in metrics

* refactor: hide satisfaction score & total response chart if rating filter is enabled

* refactor: optional chaining in group by

* fix: spacing using autofill

* test: update csat metrics tests

* test: CSAT metric card
This commit is contained in:
Shivam Mishra
2023-05-23 16:47:04 +05:30
committed by GitHub
parent 2764338453
commit 9c6c19c3e5
9 changed files with 164 additions and 12 deletions

View File

@@ -35,10 +35,10 @@ class CSATReportsAPI extends ApiClient {
}); });
} }
getMetrics({ from, to, user_ids, inbox_id, team_id } = {}) { getMetrics({ from, to, user_ids, inbox_id, team_id, rating } = {}) {
// no ratings for metrics // no ratings for metrics
return axios.get(`${this.url}/metrics`, { return axios.get(`${this.url}/metrics`, {
params: { since: from, until: to, user_ids, inbox_id, team_id }, params: { since: from, until: to, user_ids, inbox_id, team_id, rating },
}); });
} }
} }

View File

@@ -16,7 +16,7 @@
> >
{{ $t('CSAT_REPORTS.DOWNLOAD') }} {{ $t('CSAT_REPORTS.DOWNLOAD') }}
</woot-button> </woot-button>
<csat-metrics /> <csat-metrics :filters="requestPayload" />
<csat-table :page-index="pageIndex" @page-change="onPageNumberChange" /> <csat-table :page-index="pageIndex" @page-change="onPageNumberChange" />
</div> </div>
</template> </template>

View File

@@ -92,7 +92,7 @@ export default {
} }
if (!this.accountReport.data.length) return {}; if (!this.accountReport.data.length) return {};
const labels = this.accountReport.data.map(element => { const labels = this.accountReport.data.map(element => {
if (this.groupBy.period === GROUP_BY_FILTER[2].period) { if (this.groupBy?.period === GROUP_BY_FILTER[2].period) {
let week_date = new Date(fromUnixTime(element.timestamp)); let week_date = new Date(fromUnixTime(element.timestamp));
const first_day = week_date.getDate() - week_date.getDay(); const first_day = week_date.getDate() - week_date.getDay();
const last_day = first_day + 6; const last_day = first_day + 6;
@@ -105,10 +105,10 @@ export default {
'dd/MM/yy' 'dd/MM/yy'
)}`; )}`;
} }
if (this.groupBy.period === GROUP_BY_FILTER[3].period) { if (this.groupBy?.period === GROUP_BY_FILTER[3].period) {
return format(fromUnixTime(element.timestamp), 'MMM-yyyy'); return format(fromUnixTime(element.timestamp), 'MMM-yyyy');
} }
if (this.groupBy.period === GROUP_BY_FILTER[4].period) { if (this.groupBy?.period === GROUP_BY_FILTER[4].period) {
return format(fromUnixTime(element.timestamp), 'yyyy'); return format(fromUnixTime(element.timestamp), 'yyyy');
} }
return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy'); return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy');
@@ -213,7 +213,7 @@ export default {
return { return {
from, from,
to, to,
groupBy: groupBy.period, groupBy: groupBy?.period,
businessHours, businessHours,
}; };
}, },

View File

@@ -1,5 +1,10 @@
<template> <template>
<div class="medium-2 small-6 csat--metric-card"> <div
class="medium-2 small-6 csat--metric-card"
:class="{
disabled: disabled,
}"
>
<h3 class="heading"> <h3 class="heading">
<span>{{ label }}</span> <span>{{ label }}</span>
<fluent-icon <fluent-icon
@@ -29,6 +34,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
disabled: {
type: Boolean,
default: false,
},
}, },
}; };
</script> </script>
@@ -37,6 +46,13 @@ export default {
margin: 0; margin: 0;
padding: var(--space-normal); padding: var(--space-normal);
&.disabled {
// grayscale everything
filter: grayscale(100%);
opacity: 0.3;
pointer-events: none;
}
.heading { .heading {
align-items: center; align-items: center;
color: var(--color-heading); color: var(--color-heading);

View File

@@ -6,16 +6,20 @@
:value="responseCount" :value="responseCount"
/> />
<csat-metric-card <csat-metric-card
:disabled="ratingFilterEnabled"
:label="$t('CSAT_REPORTS.METRIC.SATISFACTION_SCORE.LABEL')" :label="$t('CSAT_REPORTS.METRIC.SATISFACTION_SCORE.LABEL')"
:info-text="$t('CSAT_REPORTS.METRIC.SATISFACTION_SCORE.TOOLTIP')" :info-text="$t('CSAT_REPORTS.METRIC.SATISFACTION_SCORE.TOOLTIP')"
:value="formatToPercent(satisfactionScore)" :value="ratingFilterEnabled ? '--' : formatToPercent(satisfactionScore)"
/> />
<csat-metric-card <csat-metric-card
:label="$t('CSAT_REPORTS.METRIC.RESPONSE_RATE.LABEL')" :label="$t('CSAT_REPORTS.METRIC.RESPONSE_RATE.LABEL')"
:info-text="$t('CSAT_REPORTS.METRIC.RESPONSE_RATE.TOOLTIP')" :info-text="$t('CSAT_REPORTS.METRIC.RESPONSE_RATE.TOOLTIP')"
:value="formatToPercent(responseRate)" :value="formatToPercent(responseRate)"
/> />
<div v-if="metrics.totalResponseCount" class="medium-6 report-card"> <div
v-if="metrics.totalResponseCount && !ratingFilterEnabled"
class="medium-6 report-card"
>
<h3 class="heading"> <h3 class="heading">
<div class="emoji--distribution"> <div class="emoji--distribution">
<div <div
@@ -24,7 +28,7 @@
class="emoji--distribution-item" class="emoji--distribution-item"
> >
<span class="emoji--distribution-key">{{ <span class="emoji--distribution-key">{{
csatRatings[key - 1].emoji ratingToEmoji(key)
}}</span> }}</span>
<span>{{ formatToPercent(rating) }}</span> <span>{{ formatToPercent(rating) }}</span>
</div> </div>
@@ -45,6 +49,12 @@ export default {
components: { components: {
CsatMetricCard, CsatMetricCard,
}, },
props: {
filters: {
type: Object,
required: true,
},
},
data() { data() {
return { return {
csatRatings: CSAT_RATINGS, csatRatings: CSAT_RATINGS,
@@ -57,6 +67,9 @@ export default {
satisfactionScore: 'csat/getSatisfactionScore', satisfactionScore: 'csat/getSatisfactionScore',
responseRate: 'csat/getResponseRate', responseRate: 'csat/getResponseRate',
}), }),
ratingFilterEnabled() {
return Boolean(this.filters.rating);
},
chartData() { chartData() {
return { return {
labels: ['Rating'], labels: ['Rating'],
@@ -77,6 +90,9 @@ export default {
formatToPercent(value) { formatToPercent(value) {
return value ? `${value}%` : '--'; return value ? `${value}%` : '--';
}, },
ratingToEmoji(value) {
return CSAT_RATINGS.find(rating => rating.value === Number(value)).emoji;
},
}, },
}; };
</script> </script>

View File

@@ -231,7 +231,7 @@ export default {
<style scoped> <style scoped>
.filter-container { .filter-container {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-gap: var(--space-slab); grid-gap: var(--space-slab);
margin-bottom: var(--space-normal); margin-bottom: var(--space-normal);

View File

@@ -0,0 +1,64 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import CsatMetrics from '../CsatMetrics.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
const mountParams = {
mocks: {
$t: msg => msg,
},
stubs: ['csat-metric-card', 'woot-horizontal-bar'],
};
describe('CsatMetrics.vue', () => {
let getters;
let store;
let wrapper;
const filters = { rating: 3 };
beforeEach(() => {
getters = {
'csat/getMetrics': () => ({ totalResponseCount: 100 }),
'csat/getRatingPercentage': () => ({ 1: 10, 2: 20, 3: 30, 4: 30, 5: 10 }),
'csat/getSatisfactionScore': () => 85,
'csat/getResponseRate': () => 90,
};
store = new Vuex.Store({
getters,
});
wrapper = shallowMount(CsatMetrics, {
store,
localVue,
propsData: { filters },
...mountParams,
});
});
it('computes response count correctly', () => {
expect(wrapper.vm.responseCount).toBe('100');
expect(wrapper.html()).toMatchSnapshot();
});
it('formats values to percent correctly', () => {
expect(wrapper.vm.formatToPercent(85)).toBe('85%');
expect(wrapper.vm.formatToPercent(null)).toBe('--');
});
it('maps rating value to emoji correctly', () => {
const rating = wrapper.vm.csatRatings[0]; // assuming this is { value: 1, emoji: '😡' }
expect(wrapper.vm.ratingToEmoji(rating.value)).toBe(rating.emoji);
});
it('hides report card if rating filter is enabled', () => {
expect(wrapper.find('.report-card').exists()).toBe(false);
});
it('shows report card if rating filter is not enabled', async () => {
await wrapper.setProps({ filters: {} });
expect(wrapper.find('.report-card').exists()).toBe(true);
});
});

View File

@@ -0,0 +1,46 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import CsatMetricCard from '../CsatMetricCard.vue';
import VTooltip from 'v-tooltip';
const localVue = createLocalVue();
localVue.use(VTooltip);
describe('CsatMetricCard.vue', () => {
it('renders props correctly', () => {
const label = 'Total Responses';
const value = '100';
const infoText = 'Total number of responses';
const wrapper = shallowMount(CsatMetricCard, {
propsData: { label, value, infoText },
localVue,
stubs: ['fluent-icon'],
});
expect(wrapper.find('.heading span').text()).toMatch(label);
expect(wrapper.find('.metric').text()).toMatch(value);
expect(wrapper.find('.csat--icon').classes()).toContain('has-tooltip');
});
it('adds disabled class when disabled prop is true', () => {
const wrapper = shallowMount(CsatMetricCard, {
propsData: { label: '', value: '', infoText: '', disabled: true },
localVue,
stubs: ['fluent-icon'],
});
expect(wrapper.find('.csat--metric-card').classes()).toContain('disabled');
});
it('does not add disabled class when disabled prop is false', () => {
const wrapper = shallowMount(CsatMetricCard, {
propsData: { label: '', value: '', infoText: '', disabled: false },
localVue,
stubs: ['fluent-icon'],
});
expect(wrapper.find('.csat--metric-card').classes()).not.toContain(
'disabled'
);
});
});

View File

@@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CsatMetrics.vue computes response count correctly 1`] = `
<div class="row csat--metrics-container">
<csat-metric-card-stub label="CSAT_REPORTS.METRIC.TOTAL_RESPONSES.LABEL" value="100" infotext="CSAT_REPORTS.METRIC.TOTAL_RESPONSES.TOOLTIP"></csat-metric-card-stub>
<csat-metric-card-stub label="CSAT_REPORTS.METRIC.SATISFACTION_SCORE.LABEL" value="--" infotext="CSAT_REPORTS.METRIC.SATISFACTION_SCORE.TOOLTIP" disabled="true"></csat-metric-card-stub>
<csat-metric-card-stub label="CSAT_REPORTS.METRIC.RESPONSE_RATE.LABEL" value="90%" infotext="CSAT_REPORTS.METRIC.RESPONSE_RATE.TOOLTIP"></csat-metric-card-stub>
<!---->
</div>
`;