feat: Add CSAT reports (#2608)
This commit is contained in:
@@ -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>
|
||||
@@ -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');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user