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:
@@ -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 },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
Reference in New Issue
Block a user