From d0611cb7f2dd39c8df98a7054d05848e4d54eb61 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 16 May 2025 14:18:52 +0530 Subject: [PATCH] feat: Improve CSAT responses (#11485) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request Template ## Description This PR introduces basic customization options for the CSAT survey: * **Display Type**: Option to use star ratings instead of emojis. * **Message Text**: Customize the survey message (up to 200 characters). * **Survey Rules**: Send surveys based on labels — trigger when a conversation has or doesn't have a specific label. Fixes https://linear.app/chatwoot/document/improve-csat-responses-a61cf30e054e ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? ### Loom videos **Website Channel (Widget)** https://www.loom.com/share/7f47836cde7940ae9d17b7997d060a18?sid=aad2ad0a-140a-4a09-8829-e01fa2e102c5 **Email Channel (Survey link)** https://www.loom.com/share/e92f4c4c0f73417ba300a25885e093ce?sid=4bb006f0-1c2a-4352-a232-8bf684e3d757 ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Pranav --- .../api/v1/accounts/inboxes_controller.rb | 18 +- .../filter/inputs/FilterSelect.vue | 6 +- .../components-next/message/bubbles/CSAT.vue | 33 ++- .../dashboard/i18n/locale/en/inboxMgmt.json | 31 ++- .../dashboard/settings/inbox/Settings.vue | 27 +- .../settingsPage/CustomerSatisfactionPage.vue | 233 ++++++++++++++++++ .../components/CSATDisplayTypeSelector.vue | 26 ++ .../components/CSATEmojiInput.vue | 43 ++++ .../settingsPage/components/CSATStarInput.vue | 36 +++ .../components/CustomerSatisfaction.vue | 33 ++- .../shared/components/StarRating.vue | 65 +++++ app/javascript/shared/constants/messages.js | 5 + app/javascript/survey/App.vue | 2 +- app/javascript/survey/assets/scss/woot.scss | 1 + app/javascript/survey/components/Banner.vue | 2 +- app/javascript/survey/components/Feedback.vue | 2 +- app/javascript/survey/views/Response.vue | 44 +++- .../widget/components/AgentMessageBubble.vue | 2 + app/models/inbox.rb | 1 + .../message_templates/template/csat_survey.rb | 58 ++++- app/views/api/v1/models/_inbox.json.jbuilder | 1 + .../api/v1/models/_csat_survey.json.jbuilder | 2 + ...250514045638_add_csat_config_to_inboxes.rb | 5 + db/schema.rb | 3 +- .../v1/accounts/inboxes_controller_spec.rb | 88 +++++++ .../template/csat_survey_spec.rb | 99 +++++++- 26 files changed, 812 insertions(+), 54 deletions(-) create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/CustomerSatisfactionPage.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/components/CSATDisplayTypeSelector.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/components/CSATEmojiInput.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/components/CSATStarInput.vue create mode 100644 app/javascript/shared/components/StarRating.vue create mode 100644 db/migrate/20250514045638_add_csat_config_to_inboxes.rb diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 011faaf28..61d16b2ca 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -42,7 +42,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController end def update - @inbox.update!(permitted_params.except(:channel)) + inbox_params = permitted_params.except(:channel, :csat_config) + inbox_params[:csat_config] = format_csat_config(permitted_params[:csat_config]) if permitted_params[:csat_config].present? + @inbox.update!(inbox_params) update_inbox_working_hours update_channel if channel_update_required? end @@ -121,10 +123,22 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController @inbox.channel.save! end + def format_csat_config(config) + { + display_type: config['display_type'] || 'emoji', + message: config['message'] || '', + survey_rules: { + operator: config.dig('survey_rules', 'operator') || 'contains', + values: config.dig('survey_rules', 'values') || [] + } + } + end + def inbox_attributes [:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled, :enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved, - :lock_to_single_conversation, :portal_id, :sender_name_type, :business_name] + :lock_to_single_conversation, :portal_id, :sender_name_type, :business_name, + { csat_config: [:display_type, :message, { survey_rules: [:operator, { values: [] }] }] }] end def permitted_params(channel_attributes = []) diff --git a/app/javascript/dashboard/components-next/filter/inputs/FilterSelect.vue b/app/javascript/dashboard/components-next/filter/inputs/FilterSelect.vue index db420920b..56bce8f9d 100644 --- a/app/javascript/dashboard/components-next/filter/inputs/FilterSelect.vue +++ b/app/javascript/dashboard/components-next/filter/inputs/FilterSelect.vue @@ -25,6 +25,10 @@ const props = defineProps({ type: String, default: 'faded', }, + label: { + type: String, + default: null, + }, }); const selected = defineModel({ @@ -56,7 +60,7 @@ const updateSelected = newValue => { :variant :icon="iconToRender" :trailing-icon="selectedOption.icon ? false : true" - :label="hideLabel ? null : selectedOption.label" + :label="label || (hideLabel ? null : selectedOption.label)" @click="toggle" /> diff --git a/app/javascript/dashboard/components-next/message/bubbles/CSAT.vue b/app/javascript/dashboard/components-next/message/bubbles/CSAT.vue index 211840944..6f86bd868 100644 --- a/app/javascript/dashboard/components-next/message/bubbles/CSAT.vue +++ b/app/javascript/dashboard/components-next/message/bubbles/CSAT.vue @@ -2,10 +2,10 @@ import { computed } from 'vue'; import BaseBubble from './Base.vue'; import { useI18n } from 'vue-i18n'; -import { CSAT_RATINGS } from 'shared/constants/messages'; +import { CSAT_RATINGS, CSAT_DISPLAY_TYPES } from 'shared/constants/messages'; import { useMessageContext } from '../provider.js'; -const { contentAttributes } = useMessageContext(); +const { contentAttributes, content } = useMessageContext(); const { t } = useI18n(); const response = computed(() => { @@ -16,6 +16,14 @@ const isRatingSubmitted = computed(() => { return !!response.value.rating; }); +const displayType = computed(() => { + return contentAttributes.value?.displayType || CSAT_DISPLAY_TYPES.EMOJI; +}); + +const isStarRating = computed(() => { + return displayType.value === CSAT_DISPLAY_TYPES.STAR; +}); + const rating = computed(() => { if (isRatingSubmitted.value) { return CSAT_RATINGS.find( @@ -25,16 +33,33 @@ const rating = computed(() => { return null; }); + +const starRatingValue = computed(() => { + return response.value.rating || 0; +});