feat: Add CSAT reports (#2608)

This commit is contained in:
Pranav Raj S
2021-07-14 10:20:06 +05:30
committed by GitHub
parent b7806fc210
commit cb44eb2964
34 changed files with 1120 additions and 57 deletions

View File

@@ -1,47 +0,0 @@
<template>
<div class="row--user-block">
<Thumbnail
:src="sender.thumbnail"
size="20px"
:username="sender.name"
:status="sender.availability_status"
/>
<div>
<h6 class="text-block-title text-truncate">
{{ sender.name }}
</h6>
</div>
</div>
</template>
<script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
export default {
components: {
Thumbnail,
},
props: {
sender: {
type: Object,
default: () => {},
},
},
};
</script>
<style scoped lang="scss">
@import '~dashboard/assets/scss/mixins';
.row--user-block {
align-items: center;
display: flex;
text-align: left;
.user-name {
margin: 0;
text-transform: capitalize;
}
.user-thumbnail-box {
margin-right: var(--space-small);
}
}
</style>

View File

@@ -22,7 +22,7 @@ import Spinner from 'shared/components/Spinner.vue';
import Label from 'dashboard/components/ui/Label';
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
import WootButton from 'dashboard/components/ui/WootButton.vue';
import CampaignSender from './CampaignSender';
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName';
export default {
components: {
@@ -95,7 +95,7 @@ export default {
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.SENDER'),
align: 'left',
renderBodyCell: ({ row }) => {
if (row.sender) return <CampaignSender sender={row.sender} />;
if (row.sender) return <UserAvatarWithName user={row.sender} />;
return this.$t('CAMPAIGN.LIST.SENDER.BOT');
},
},

View File

@@ -0,0 +1,33 @@
<template>
<div class="column content-box">
<csat-metrics />
<csat-table :page-index="pageIndex" @page-change="onPageNumberChange" />
</div>
</template>
<script>
import CsatMetrics from './components/CsatMetrics';
import CsatTable from './components/CsatTable';
export default {
name: 'CsatResponses',
components: {
CsatMetrics,
CsatTable,
},
data() {
return { pageIndex: 1 };
},
mounted() {
this.$store.dispatch('csat/getMetrics');
this.getData();
},
methods: {
getData() {
this.$store.dispatch('csat/get', { page: this.pageIndex });
},
onPageNumberChange(pageIndex) {
this.pageIndex = pageIndex;
this.getData();
},
},
};
</script>

View File

@@ -0,0 +1,43 @@
import Vue from 'vue';
import VTooltip from 'v-tooltip';
import CsatMetricCard from './CsatMetricCard';
Vue.use(VTooltip, { defaultHtml: false });
export default {
title: 'Components/CSAT/Metrics Card',
component: CsatMetricCard,
argTypes: {
label: {
defaultValue: '',
control: {
type: 'text',
},
},
value: {
defaultValue: '',
control: {
type: 'text',
},
},
infoText: {
defaultValue: '',
control: {
type: 'text',
},
},
},
};
const Template = (_, { argTypes }) => ({
props: Object.keys(argTypes),
components: { CsatMetricCard },
template: '<csat-metric-card v-bind="$props" />',
});
export const CsatMetricCardTemplate = Template.bind({});
CsatMetricCardTemplate.args = {
infoText: 'No. of responses / No. of survey messages sent * 100',
label: 'Satisfaction Score',
value: '98.5',
};

View File

@@ -0,0 +1,55 @@
<template>
<div class="medium-2 small-6 csat--metric-card">
<h3 class="heading">
<span>{{ label }}</span>
<i v-tooltip="infoText" class="csat--icon ion-ios-information" />
</h3>
<h4 class="metric">
{{ value }}
</h4>
</div>
</template>
<script>
export default {
props: {
label: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
infoText: {
type: String,
required: true,
},
},
};
</script>
<style lang="scss" scoped>
.csat--metric-card {
margin: 0;
padding: var(--space-normal) var(--space-small) var(--space-normal)
var(--space-two);
.heading {
color: var(--color-heading);
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
margin: 0;
}
.metric {
font-size: var(--font-size-bigger);
font-weight: var(--font-weight-feather);
margin-bottom: 0;
margin-top: var(--space-smaller);
}
}
.csat--icon {
color: var(--b-400);
margin-left: var(--space-micro);
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<div class="row csat--metrics-container">
<csat-metric-card
:label="$t('CSAT_REPORTS.METRIC.TOTAL_RESPONSES.LABEL')"
:info-text="$t('CSAT_REPORTS.METRIC.TOTAL_RESPONSES.TOOLTIP')"
:value="responseCount"
/>
<csat-metric-card
:label="$t('CSAT_REPORTS.METRIC.SATISFACTION_SCORE.LABEL')"
:info-text="$t('CSAT_REPORTS.METRIC.SATISFACTION_SCORE.TOOLTIP')"
:value="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">
<h3 class="heading">
<div class="emoji--distribution">
<div
v-for="(rating, key, index) in ratingPercentage"
:key="rating + key + index"
class="emoji--distribution-item"
>
<span class="emoji--distribution-key">{{
csatRatings[key - 1].emoji
}}</span>
<span>{{ formatToPercent(rating) }}</span>
</div>
</div>
</h3>
<div class="emoji--distribution-chart">
<woot-horizontal-bar :collection="chartData" :height="24" />
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import CsatMetricCard from './CsatMetricCard';
import { CSAT_RATINGS } from 'shared/constants/messages';
export default {
components: {
CsatMetricCard,
},
data() {
return {
csatRatings: CSAT_RATINGS,
};
},
computed: {
...mapGetters({
metrics: 'csat/getMetrics',
ratingPercentage: 'csat/getRatingPercentage',
satisfactionScore: 'csat/getSatisfactionScore',
responseRate: 'csat/getResponseRate',
}),
chartData() {
return {
labels: ['Rating'],
datasets: CSAT_RATINGS.map((rating, index) => ({
label: rating.emoji,
data: [this.ratingPercentage[index + 1]],
backgroundColor: rating.color,
})),
};
},
responseCount() {
return this.metrics.totalResponseCount
? this.metrics.totalResponseCount.toLocaleString()
: '--';
},
},
methods: {
formatToPercent(value) {
return value ? `${value}%` : '--';
},
},
};
</script>
<style lang="scss" scoped>
.csat--metrics-container {
background: var(--white);
margin-bottom: var(--space-two);
border-radius: var(--border-radius-normal);
border: 1px solid var(--color-border);
padding: var(--space-normal);
}
.emoji--distribution {
display: flex;
justify-content: flex-end;
.emoji--distribution-item {
padding-left: var(--space-normal);
}
}
.emoji--distribution-chart {
margin-top: var(--space-small);
}
.emoji--distribution-key {
margin-right: var(--space-micro);
}
</style>

View File

@@ -0,0 +1,191 @@
<template>
<div class="csat--table-container">
<ve-table
max-height="calc(100vh - 30rem)"
:fixed-header="true"
:columns="columns"
:table-data="tableData"
/>
<div v-show="!tableData.length" class="csat--empty-records">
{{ $t('CSAT_REPORTS.NO_RECORDS') }}
</div>
<div v-if="metrics.totalResponseCount" class="table-pagination">
<ve-pagination
:total="metrics.totalResponseCount"
:page-index="pageIndex"
:page-size="25"
:page-size-option="[25]"
@on-page-number-change="onPageNumberChange"
/>
</div>
</div>
</template>
<script>
import { VeTable, VePagination } from 'vue-easytable';
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName';
import { CSAT_RATINGS } from 'shared/constants/messages';
import { mapGetters } from 'vuex';
export default {
components: {
VeTable,
VePagination,
},
props: {
pageIndex: {
type: Number,
default: 1,
},
},
computed: {
...mapGetters({
uiFlags: 'csat/getUIFlags',
csatResponses: 'csat/getCSATResponses',
metrics: 'csat/getMetrics',
}),
columns() {
return [
{
field: 'contact',
key: 'contact',
title: this.$t('CSAT_REPORTS.TABLE.HEADER.CONTACT_NAME'),
align: 'left',
width: 200,
renderBodyCell: ({ row }) => {
if (row.contact) {
return <UserAvatarWithName size="24px" user={row.contact} />;
}
return '---';
},
},
{
field: 'assignedAgent',
key: 'assignedAgent',
title: this.$t('CSAT_REPORTS.TABLE.HEADER.AGENT_NAME'),
align: 'left',
width: 200,
renderBodyCell: ({ row }) => {
if (row.assignedAgent) {
return (
<UserAvatarWithName size="24px" user={row.assignedAgent} />
);
}
return '---';
},
},
{
field: 'rating',
key: 'rating',
title: this.$t('CSAT_REPORTS.TABLE.HEADER.RATING'),
align: 'center',
width: 80,
renderBodyCell: ({ row }) => {
const [ratingObject = {}] = CSAT_RATINGS.filter(
rating => rating.value === row.rating
);
return (
<span class="emoji-response">{ratingObject.emoji || '---'}</span>
);
},
},
{
field: 'feedbackText',
key: 'feedbackText',
title: this.$t('CSAT_REPORTS.TABLE.HEADER.FEEDBACK_TEXT'),
align: 'left',
},
{
field: 'converstionId',
key: 'converstionId',
title: '',
align: 'left',
width: 100,
renderBodyCell: ({ row }) => {
const routerParams = {
name: 'inbox_conversation',
params: { conversation_id: row.conversationId },
};
return (
<router-link to={routerParams}>
{`#${row.conversationId}`}
</router-link>
);
},
},
];
},
tableData() {
return this.csatResponses.map(response => ({
contact: response.contact,
assignedAgent: response.assigned_agent,
rating: response.rating,
feedbackText: response.feedback_message || '---',
conversationId: response.conversation_id,
}));
},
},
methods: {
onPageNumberChange(pageIndex) {
this.$emit('page-change', pageIndex);
},
},
};
</script>
<style lang="scss" scoped>
.csat--table-container {
display: flex;
flex-direction: column;
flex: 1;
.ve-table {
background: var(--white);
&::v-deep {
.ve-table-container {
border-radius: var(--border-radius-normal);
border: 1px solid var(--color-border) !important;
}
th.ve-table-header-th {
font-size: var(--font-size-mini) !important;
padding: var(--space-normal) !important;
}
td.ve-table-body-td {
padding: var(--space-small) var(--space-normal) !important;
}
}
}
&::v-deep .ve-pagination {
background-color: transparent;
}
&::v-deep .ve-pagination-select {
display: none;
}
.emoji-response {
font-size: var(--font-size-large);
}
.table-pagination {
margin-top: var(--space-normal);
text-align: right;
}
}
.csat--empty-records {
align-items: center;
background-color: var(--white);
border: 1px solid var(--color-border);
border-top: 0;
color: var(--b-600);
display: flex;
font-size: var(--font-size-small);
height: 20rem;
justify-content: center;
margin-top: -1px;
width: 100%;
}
</style>

View File

@@ -1,4 +1,5 @@
import Index from './Index';
import CsatResponses from './CsatResponses';
import SettingsContent from '../Wrapper';
import { frontendURL } from '../../../../helper/URLHelper';
@@ -9,17 +10,36 @@ export default {
component: SettingsContent,
props: {
headerTitle: 'REPORT.HEADER',
headerButtonText: 'REPORT.HEADER_BTN_TXT',
icon: 'ion-arrow-graph-up-right',
},
children: [
{
path: '',
redirect: 'overview',
},
{
path: 'overview',
name: 'settings_account_reports',
roles: ['administrator'],
component: Index,
},
],
},
{
path: frontendURL('accounts/:accountId/reports'),
component: SettingsContent,
props: {
headerTitle: 'CSAT_REPORTS.HEADER',
icon: 'ion-happy-outline',
},
children: [
{
path: 'csat',
name: 'csat_reports',
roles: ['administrator'],
component: CsatResponses,
},
],
},
],
};