feat: Add customer satisfaction component (#2456)
This commit is contained in:
parent
a357b657a8
commit
29b9915d3b
11 changed files with 225 additions and 6 deletions
|
@ -30,6 +30,7 @@ exclude_patterns:
|
||||||
- "**/*.md"
|
- "**/*.md"
|
||||||
- "**/*.yml"
|
- "**/*.yml"
|
||||||
- "app/javascript/dashboard/i18n/locale"
|
- "app/javascript/dashboard/i18n/locale"
|
||||||
|
- "**/*.stories.js"
|
||||||
- "stories/**/*"
|
- "stories/**/*"
|
||||||
- "**/*.stories.js"
|
- "**/*.stories.js"
|
||||||
- "**/stories/"
|
- "**/stories/"
|
||||||
|
|
6
app/javascript/dashboard/i18n/locale/en/csatMgmt.json
Normal file
6
app/javascript/dashboard/i18n/locale/en/csatMgmt.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"CSAT": {
|
||||||
|
"TITLE": "Rate your conversation",
|
||||||
|
"PLACEHOLDER": "Tell us more..."
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ import { default as _settings } from './settings.json';
|
||||||
import { default as _signup } from './signup.json';
|
import { default as _signup } from './signup.json';
|
||||||
import { default as _teamsSettings } from './teamsSettings.json';
|
import { default as _teamsSettings } from './teamsSettings.json';
|
||||||
import { default as _integrationApps } from './integrationApps.json';
|
import { default as _integrationApps } from './integrationApps.json';
|
||||||
|
import { default as _csatMgmtMgmt } from './csatMgmt.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
..._agentMgmt,
|
..._agentMgmt,
|
||||||
|
@ -36,4 +37,5 @@ export default {
|
||||||
..._signup,
|
..._signup,
|
||||||
..._teamsSettings,
|
..._teamsSettings,
|
||||||
..._integrationApps,
|
..._integrationApps,
|
||||||
|
..._csatMgmtMgmt,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import CustomerSatisfaction from './CustomerSatisfaction';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/CustomerSatisfaction',
|
||||||
|
component: CustomerSatisfaction,
|
||||||
|
argTypes: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args, { argTypes }) => ({
|
||||||
|
props: Object.keys(argTypes),
|
||||||
|
components: { CustomerSatisfaction },
|
||||||
|
template: '<customer-satisfaction />',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const item = Template.bind({});
|
||||||
|
item.args = {
|
||||||
|
onClick: action('Selected'),
|
||||||
|
};
|
137
app/javascript/shared/components/CustomerSatisfaction.vue
Normal file
137
app/javascript/shared/components/CustomerSatisfaction.vue
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
<template>
|
||||||
|
<div class="customer-satisfcation">
|
||||||
|
<div class="title">
|
||||||
|
{{ $t('CSAT.TITLE') }}
|
||||||
|
</div>
|
||||||
|
<div class="ratings">
|
||||||
|
<button
|
||||||
|
v-for="rating in ratings"
|
||||||
|
:key="rating.key"
|
||||||
|
class="emoji-button"
|
||||||
|
:class="{ selected: rating.key === selectedRating }"
|
||||||
|
@click="selectRating(rating)"
|
||||||
|
>
|
||||||
|
{{ rating.emoji }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
v-if="!hasSubmitted"
|
||||||
|
class="feedback-form"
|
||||||
|
@submit.prevent="onSubmit()"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="feedback"
|
||||||
|
class="form-input"
|
||||||
|
:placeholder="$t('CSAT.PLACEHOLDER')"
|
||||||
|
@keyup.enter="onSubmit"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
:disabled="!selectedRating"
|
||||||
|
:style="{ background: widgetColor, borderColor: widgetColor }"
|
||||||
|
>
|
||||||
|
<i v-if="!isUpdating" class="ion-ios-arrow-forward" />
|
||||||
|
<spinner v-else />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import Spinner from 'shared/components/Spinner';
|
||||||
|
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Spinner,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
email: '',
|
||||||
|
ratings: CSAT_RATINGS,
|
||||||
|
selectedRating: null,
|
||||||
|
isUpdating: false,
|
||||||
|
feedback: '',
|
||||||
|
hasSubmitted: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
widgetColor: 'appConfig/getWidgetColor',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onSubmit() {},
|
||||||
|
selectRating(rating) {
|
||||||
|
this.selectedRating = rating.key;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~widget/assets/scss/variables.scss';
|
||||||
|
@import '~widget/assets/scss/mixins.scss';
|
||||||
|
|
||||||
|
.customer-satisfcation {
|
||||||
|
@include light-shadow;
|
||||||
|
background: $color-white;
|
||||||
|
border-bottom-left-radius: $space-smaller;
|
||||||
|
color: $color-body;
|
||||||
|
border-top: $space-micro solid $color-woot;
|
||||||
|
border-radius: $space-one;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1.5;
|
||||||
|
width: 75%;
|
||||||
|
.title {
|
||||||
|
font-size: $font-size-default;
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
padding-top: $space-two;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.ratings {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: $space-two $space-normal;
|
||||||
|
|
||||||
|
.emoji-button {
|
||||||
|
font-size: $font-size-big;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
filter: grayscale(100%);
|
||||||
|
&.selected {
|
||||||
|
filter: grayscale(0%);
|
||||||
|
transform: scale(1.32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.feedback-form {
|
||||||
|
display: flex;
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
padding: $space-one;
|
||||||
|
border-top: 1px solid $color-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
font-size: $font-size-large;
|
||||||
|
height: auto;
|
||||||
|
margin-left: -1px;
|
||||||
|
appearance: none;
|
||||||
|
.spinner {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -12,3 +12,31 @@ export const MESSAGE_TYPE = {
|
||||||
};
|
};
|
||||||
// Size in mega bytes
|
// Size in mega bytes
|
||||||
export const MAXIMUM_FILE_UPLOAD_SIZE = 40;
|
export const MAXIMUM_FILE_UPLOAD_SIZE = 40;
|
||||||
|
|
||||||
|
export const CSAT_RATINGS = [
|
||||||
|
{
|
||||||
|
key: 'disappointed',
|
||||||
|
emoji: '😞',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'expressionless',
|
||||||
|
emoji: '😑',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'neutral',
|
||||||
|
emoji: '😐',
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'grinning',
|
||||||
|
emoji: '😀',
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'smiling',
|
||||||
|
emoji: '😍',
|
||||||
|
value: 4,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="chat-bubble-wrap">
|
<div class="chat-bubble-wrap">
|
||||||
<div
|
<div
|
||||||
v-if="!isCards && !isOptions && !isForm && !isArticle"
|
v-if="
|
||||||
|
!isCards && !isOptions && !isForm && !isArticle && !isCards && !isCSAT
|
||||||
|
"
|
||||||
class="chat-bubble agent"
|
class="chat-bubble agent"
|
||||||
>
|
>
|
||||||
<div class="message-content" v-html="formatMessage(message, false)"></div>
|
<div class="message-content" v-html="formatMessage(message, false)"></div>
|
||||||
|
@ -42,6 +44,7 @@
|
||||||
<div v-if="isArticle">
|
<div v-if="isArticle">
|
||||||
<chat-article :items="messageContentAttributes.items"></chat-article>
|
<chat-article :items="messageContentAttributes.items"></chat-article>
|
||||||
</div>
|
</div>
|
||||||
|
<customer-satisfaction v-if="isCSAT" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -52,6 +55,7 @@ import ChatForm from 'shared/components/ChatForm';
|
||||||
import ChatOptions from 'shared/components/ChatOptions';
|
import ChatOptions from 'shared/components/ChatOptions';
|
||||||
import ChatArticle from './template/Article';
|
import ChatArticle from './template/Article';
|
||||||
import EmailInput from './template/EmailInput';
|
import EmailInput from './template/EmailInput';
|
||||||
|
import CustomerSatisfaction from 'shared/components/CustomerSatisfaction';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AgentMessageBubble',
|
name: 'AgentMessageBubble',
|
||||||
|
@ -61,13 +65,14 @@ export default {
|
||||||
ChatForm,
|
ChatForm,
|
||||||
ChatOptions,
|
ChatOptions,
|
||||||
EmailInput,
|
EmailInput,
|
||||||
|
CustomerSatisfaction,
|
||||||
},
|
},
|
||||||
mixins: [messageFormatterMixin],
|
mixins: [messageFormatterMixin],
|
||||||
props: {
|
props: {
|
||||||
message: String,
|
message: { type: String, default: null },
|
||||||
contentType: String,
|
contentType: { type: String, default: null },
|
||||||
messageType: Number,
|
messageType: { type: Number, default: null },
|
||||||
messageId: Number,
|
messageId: { type: Number, default: null },
|
||||||
messageContentAttributes: {
|
messageContentAttributes: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
|
@ -92,6 +97,9 @@ export default {
|
||||||
isArticle() {
|
isArticle() {
|
||||||
return this.contentType === 'article';
|
return this.contentType === 'article';
|
||||||
},
|
},
|
||||||
|
isCSAT() {
|
||||||
|
return this.contentType === 'input_csat';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onResponse(messageResponse) {
|
onResponse(messageResponse) {
|
||||||
|
|
|
@ -56,5 +56,9 @@
|
||||||
"INVALID": {
|
"INVALID": {
|
||||||
"FIELD": "Invalid field"
|
"FIELD": "Invalid field"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"CSAT": {
|
||||||
|
"TITLE": "Rate your conversation",
|
||||||
|
"PLACEHOLDER": "Tell us more..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,8 @@ class Message < ApplicationRecord
|
||||||
cards: 5,
|
cards: 5,
|
||||||
form: 6,
|
form: 6,
|
||||||
article: 7,
|
article: 7,
|
||||||
incoming_email: 8
|
incoming_email: 8,
|
||||||
|
input_csat: 9
|
||||||
}
|
}
|
||||||
enum status: { sent: 0, delivered: 1, read: 2, failed: 3 }
|
enum status: { sent: 0, delivered: 1, read: 2, failed: 3 }
|
||||||
# [:submitted_email, :items, :submitted_values] : Used for bot message types
|
# [:submitted_email, :items, :submitted_values] : Used for bot message types
|
||||||
|
|
|
@ -72,6 +72,8 @@ unless Rails.env.production?
|
||||||
WootMessageSeeder.create_sample_form_message conversation
|
WootMessageSeeder.create_sample_form_message conversation
|
||||||
# articles
|
# articles
|
||||||
WootMessageSeeder.create_sample_articles_message conversation
|
WootMessageSeeder.create_sample_articles_message conversation
|
||||||
|
# csat
|
||||||
|
WootMessageSeeder.create_sample_csat_collect_message conversation
|
||||||
|
|
||||||
CannedResponse.create!(account: account, short_code: 'start', content: 'Hello welcome to chatwoot.')
|
CannedResponse.create!(account: account, short_code: 'start', content: 'Hello welcome to chatwoot.')
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,17 @@ module WootMessageSeeder
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.create_sample_csat_collect_message(conversation)
|
||||||
|
Message.create!(
|
||||||
|
account: conversation.account,
|
||||||
|
inbox: conversation.inbox,
|
||||||
|
conversation: conversation,
|
||||||
|
message_type: :template,
|
||||||
|
content_type: :input_csat,
|
||||||
|
content: 'Please rate the support'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def self.create_sample_cards_message(conversation)
|
def self.create_sample_cards_message(conversation)
|
||||||
Message.create!(
|
Message.create!(
|
||||||
account: conversation.account,
|
account: conversation.account,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue