mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 04:27:53 +00:00
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
|
||||
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') }}
|
||||
</woot-button>
|
||||
<csat-metrics />
|
||||
<csat-metrics :filters="requestPayload" />
|
||||
<csat-table :page-index="pageIndex" @page-change="onPageNumberChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -92,7 +92,7 @@ export default {
|
||||
}
|
||||
if (!this.accountReport.data.length) return {};
|
||||
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));
|
||||
const first_day = week_date.getDate() - week_date.getDay();
|
||||
const last_day = first_day + 6;
|
||||
@@ -105,10 +105,10 @@ export default {
|
||||
'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');
|
||||
}
|
||||
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), 'dd-MMM-yyyy');
|
||||
@@ -213,7 +213,7 @@ export default {
|
||||
return {
|
||||
from,
|
||||
to,
|
||||
groupBy: groupBy.period,
|
||||
groupBy: groupBy?.period,
|
||||
businessHours,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<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">
|
||||
<span>{{ label }}</span>
|
||||
<fluent-icon
|
||||
@@ -29,6 +34,10 @@ export default {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -37,6 +46,13 @@ export default {
|
||||
margin: 0;
|
||||
padding: var(--space-normal);
|
||||
|
||||
&.disabled {
|
||||
// grayscale everything
|
||||
filter: grayscale(100%);
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.heading {
|
||||
align-items: center;
|
||||
color: var(--color-heading);
|
||||
|
||||
@@ -6,16 +6,20 @@
|
||||
:value="responseCount"
|
||||
/>
|
||||
<csat-metric-card
|
||||
:disabled="ratingFilterEnabled"
|
||||
:label="$t('CSAT_REPORTS.METRIC.SATISFACTION_SCORE.LABEL')"
|
||||
:info-text="$t('CSAT_REPORTS.METRIC.SATISFACTION_SCORE.TOOLTIP')"
|
||||
:value="formatToPercent(satisfactionScore)"
|
||||
:value="ratingFilterEnabled ? '--' : formatToPercent(satisfactionScore)"
|
||||
/>
|
||||
<csat-metric-card
|
||||
:label="$t('CSAT_REPORTS.METRIC.RESPONSE_RATE.LABEL')"
|
||||
:info-text="$t('CSAT_REPORTS.METRIC.RESPONSE_RATE.TOOLTIP')"
|
||||
: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">
|
||||
<div class="emoji--distribution">
|
||||
<div
|
||||
@@ -24,7 +28,7 @@
|
||||
class="emoji--distribution-item"
|
||||
>
|
||||
<span class="emoji--distribution-key">{{
|
||||
csatRatings[key - 1].emoji
|
||||
ratingToEmoji(key)
|
||||
}}</span>
|
||||
<span>{{ formatToPercent(rating) }}</span>
|
||||
</div>
|
||||
@@ -45,6 +49,12 @@ export default {
|
||||
components: {
|
||||
CsatMetricCard,
|
||||
},
|
||||
props: {
|
||||
filters: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
csatRatings: CSAT_RATINGS,
|
||||
@@ -57,6 +67,9 @@ export default {
|
||||
satisfactionScore: 'csat/getSatisfactionScore',
|
||||
responseRate: 'csat/getResponseRate',
|
||||
}),
|
||||
ratingFilterEnabled() {
|
||||
return Boolean(this.filters.rating);
|
||||
},
|
||||
chartData() {
|
||||
return {
|
||||
labels: ['Rating'],
|
||||
@@ -77,6 +90,9 @@ export default {
|
||||
formatToPercent(value) {
|
||||
return value ? `${value}%` : '--';
|
||||
},
|
||||
ratingToEmoji(value) {
|
||||
return CSAT_RATINGS.find(rating => rating.value === Number(value)).emoji;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -231,7 +231,7 @@ export default {
|
||||
<style scoped>
|
||||
.filter-container {
|
||||
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);
|
||||
|
||||
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