mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-04 04:57:51 +00:00 
			
		
		
		
	feat: Improve CSAT responses (#11485)
# 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 <pranavrajs@gmail.com>
This commit is contained in:
		@@ -42,7 +42,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
 | 
				
			|||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def update
 | 
					  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_inbox_working_hours
 | 
				
			||||||
    update_channel if channel_update_required?
 | 
					    update_channel if channel_update_required?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -121,10 +123,22 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
 | 
				
			|||||||
    @inbox.channel.save!
 | 
					    @inbox.channel.save!
 | 
				
			||||||
  end
 | 
					  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
 | 
					  def inbox_attributes
 | 
				
			||||||
    [:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
 | 
					    [: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,
 | 
					     :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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def permitted_params(channel_attributes = [])
 | 
					  def permitted_params(channel_attributes = [])
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,6 +25,10 @@ const props = defineProps({
 | 
				
			|||||||
    type: String,
 | 
					    type: String,
 | 
				
			||||||
    default: 'faded',
 | 
					    default: 'faded',
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  label: {
 | 
				
			||||||
 | 
					    type: String,
 | 
				
			||||||
 | 
					    default: null,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const selected = defineModel({
 | 
					const selected = defineModel({
 | 
				
			||||||
@@ -56,7 +60,7 @@ const updateSelected = newValue => {
 | 
				
			|||||||
          :variant
 | 
					          :variant
 | 
				
			||||||
          :icon="iconToRender"
 | 
					          :icon="iconToRender"
 | 
				
			||||||
          :trailing-icon="selectedOption.icon ? false : true"
 | 
					          :trailing-icon="selectedOption.icon ? false : true"
 | 
				
			||||||
          :label="hideLabel ? null : selectedOption.label"
 | 
					          :label="label || (hideLabel ? null : selectedOption.label)"
 | 
				
			||||||
          @click="toggle"
 | 
					          @click="toggle"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </slot>
 | 
					      </slot>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,10 +2,10 @@
 | 
				
			|||||||
import { computed } from 'vue';
 | 
					import { computed } from 'vue';
 | 
				
			||||||
import BaseBubble from './Base.vue';
 | 
					import BaseBubble from './Base.vue';
 | 
				
			||||||
import { useI18n } from 'vue-i18n';
 | 
					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';
 | 
					import { useMessageContext } from '../provider.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { contentAttributes } = useMessageContext();
 | 
					const { contentAttributes, content } = useMessageContext();
 | 
				
			||||||
const { t } = useI18n();
 | 
					const { t } = useI18n();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const response = computed(() => {
 | 
					const response = computed(() => {
 | 
				
			||||||
@@ -16,6 +16,14 @@ const isRatingSubmitted = computed(() => {
 | 
				
			|||||||
  return !!response.value.rating;
 | 
					  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(() => {
 | 
					const rating = computed(() => {
 | 
				
			||||||
  if (isRatingSubmitted.value) {
 | 
					  if (isRatingSubmitted.value) {
 | 
				
			||||||
    return CSAT_RATINGS.find(
 | 
					    return CSAT_RATINGS.find(
 | 
				
			||||||
@@ -25,16 +33,33 @@ const rating = computed(() => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return null;
 | 
					  return null;
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const starRatingValue = computed(() => {
 | 
				
			||||||
 | 
					  return response.value.rating || 0;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <BaseBubble class="px-4 py-3" data-bubble-name="csat">
 | 
					  <BaseBubble class="px-4 py-3" data-bubble-name="csat">
 | 
				
			||||||
    <h4>{{ t('CONVERSATION.CSAT_REPLY_MESSAGE') }}</h4>
 | 
					    <h4>{{ content || t('CONVERSATION.CSAT_REPLY_MESSAGE') }}</h4>
 | 
				
			||||||
    <dl v-if="isRatingSubmitted" class="mt-4">
 | 
					    <dl v-if="isRatingSubmitted" class="mt-4">
 | 
				
			||||||
      <dt class="text-n-slate-11 italic">
 | 
					      <dt class="text-n-slate-11 italic">
 | 
				
			||||||
        {{ t('CONVERSATION.RATING_TITLE') }}
 | 
					        {{ t('CONVERSATION.RATING_TITLE') }}
 | 
				
			||||||
      </dt>
 | 
					      </dt>
 | 
				
			||||||
      <dd>{{ t(rating.translationKey) }}</dd>
 | 
					      <dd v-if="!isStarRating">
 | 
				
			||||||
 | 
					        {{ t(rating.translationKey) }}
 | 
				
			||||||
 | 
					      </dd>
 | 
				
			||||||
 | 
					      <dd v-else class="flex mt-1">
 | 
				
			||||||
 | 
					        <span v-for="n in 5" :key="n" class="text-2xl mr-1">
 | 
				
			||||||
 | 
					          <i
 | 
				
			||||||
 | 
					            :class="[
 | 
				
			||||||
 | 
					              n <= starRatingValue
 | 
				
			||||||
 | 
					                ? 'i-ri-star-fill text-n-amber-9'
 | 
				
			||||||
 | 
					                : 'i-ri-star-line text-n-slate-10',
 | 
				
			||||||
 | 
					            ]"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      </dd>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <dt v-if="response.feedbackMessage" class="text-n-slate-11 italic mt-2">
 | 
					      <dt v-if="response.feedbackMessage" class="text-n-slate-11 italic mt-2">
 | 
				
			||||||
        {{ t('CONVERSATION.FEEDBACK_TITLE') }}
 | 
					        {{ t('CONVERSATION.FEEDBACK_TITLE') }}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -481,7 +481,8 @@
 | 
				
			|||||||
      "PRE_CHAT_FORM": "Pre Chat Form",
 | 
					      "PRE_CHAT_FORM": "Pre Chat Form",
 | 
				
			||||||
      "BUSINESS_HOURS": "Business Hours",
 | 
					      "BUSINESS_HOURS": "Business Hours",
 | 
				
			||||||
      "WIDGET_BUILDER": "Widget Builder",
 | 
					      "WIDGET_BUILDER": "Widget Builder",
 | 
				
			||||||
      "BOT_CONFIGURATION": "Bot Configuration"
 | 
					      "BOT_CONFIGURATION": "Bot Configuration",
 | 
				
			||||||
 | 
					      "CSAT": "CSAT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "SETTINGS": "Settings",
 | 
					    "SETTINGS": "Settings",
 | 
				
			||||||
    "FEATURES": {
 | 
					    "FEATURES": {
 | 
				
			||||||
@@ -502,9 +503,7 @@
 | 
				
			|||||||
      "ENABLE_EMAIL_COLLECT_BOX": "Enable email collect box",
 | 
					      "ENABLE_EMAIL_COLLECT_BOX": "Enable email collect box",
 | 
				
			||||||
      "ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Enable or disable email collect box on new conversation",
 | 
					      "ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Enable or disable email collect box on new conversation",
 | 
				
			||||||
      "AUTO_ASSIGNMENT": "Enable auto assignment",
 | 
					      "AUTO_ASSIGNMENT": "Enable auto assignment",
 | 
				
			||||||
      "ENABLE_CSAT": "Enable CSAT",
 | 
					 | 
				
			||||||
      "SENDER_NAME_SECTION": "Enable Agent Name in Email",
 | 
					      "SENDER_NAME_SECTION": "Enable Agent Name in Email",
 | 
				
			||||||
      "ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
 | 
					 | 
				
			||||||
      "SENDER_NAME_SECTION_TEXT": "Enable/Disable showing Agent's name in email, if disabled it will show business name",
 | 
					      "SENDER_NAME_SECTION_TEXT": "Enable/Disable showing Agent's name in email, if disabled it will show business name",
 | 
				
			||||||
      "ENABLE_CONTINUITY_VIA_EMAIL": "Enable conversation continuity via email",
 | 
					      "ENABLE_CONTINUITY_VIA_EMAIL": "Enable conversation continuity via email",
 | 
				
			||||||
      "ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Conversations will continue over email if the contact email address is available.",
 | 
					      "ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Conversations will continue over email if the contact email address is available.",
 | 
				
			||||||
@@ -578,6 +577,32 @@
 | 
				
			|||||||
        "LABEL": "Visitors should provide their name and email address before starting the chat"
 | 
					        "LABEL": "Visitors should provide their name and email address before starting the chat"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "CSAT": {
 | 
				
			||||||
 | 
					      "TITLE": "Enable CSAT",
 | 
				
			||||||
 | 
					      "SUBTITLE": "Automatically trigger CSAT surveys at the end of conversations to understand how customers feel about their support experience. Track satisfaction trends and identify areas for improvement over time.",
 | 
				
			||||||
 | 
					      "DISPLAY_TYPE": {
 | 
				
			||||||
 | 
					        "LABEL": "Display type"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "MESSAGE": {
 | 
				
			||||||
 | 
					        "LABEL": "Message",
 | 
				
			||||||
 | 
					        "PLACEHOLDER": "Please enter a message to show users with the form"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "SURVEY_RULE": {
 | 
				
			||||||
 | 
					        "LABEL": "Survey rule",
 | 
				
			||||||
 | 
					        "DESCRIPTION_PREFIX": "Send the survey if the conversation",
 | 
				
			||||||
 | 
					        "DESCRIPTION_SUFFIX": "any of the labels",
 | 
				
			||||||
 | 
					        "OPERATOR": {
 | 
				
			||||||
 | 
					          "CONTAINS": "contains",
 | 
				
			||||||
 | 
					          "DOES_NOT_CONTAINS": "does not contain"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "SELECT_PLACEHOLDER": "select labels"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "NOTE": "Note: CSAT surveys are sent only once per conversation",
 | 
				
			||||||
 | 
					      "API": {
 | 
				
			||||||
 | 
					        "SUCCESS_MESSAGE": "CSAT settings updated successfully",
 | 
				
			||||||
 | 
					        "ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "BUSINESS_HOURS": {
 | 
					    "BUSINESS_HOURS": {
 | 
				
			||||||
      "TITLE": "Set your availability",
 | 
					      "TITLE": "Set your availability",
 | 
				
			||||||
      "SUBTITLE": "Set your availability on your livechat widget",
 | 
					      "SUBTITLE": "Set your availability on your livechat widget",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@ import PreChatFormSettings from './PreChatForm/Settings.vue';
 | 
				
			|||||||
import WeeklyAvailability from './components/WeeklyAvailability.vue';
 | 
					import WeeklyAvailability from './components/WeeklyAvailability.vue';
 | 
				
			||||||
import GreetingsEditor from 'shared/components/GreetingsEditor.vue';
 | 
					import GreetingsEditor from 'shared/components/GreetingsEditor.vue';
 | 
				
			||||||
import ConfigurationPage from './settingsPage/ConfigurationPage.vue';
 | 
					import ConfigurationPage from './settingsPage/ConfigurationPage.vue';
 | 
				
			||||||
 | 
					import CustomerSatisfactionPage from './settingsPage/CustomerSatisfactionPage.vue';
 | 
				
			||||||
import CollaboratorsPage from './settingsPage/CollaboratorsPage.vue';
 | 
					import CollaboratorsPage from './settingsPage/CollaboratorsPage.vue';
 | 
				
			||||||
import WidgetBuilder from './WidgetBuilder.vue';
 | 
					import WidgetBuilder from './WidgetBuilder.vue';
 | 
				
			||||||
import BotConfiguration from './components/BotConfiguration.vue';
 | 
					import BotConfiguration from './components/BotConfiguration.vue';
 | 
				
			||||||
@@ -28,6 +29,7 @@ export default {
 | 
				
			|||||||
    BotConfiguration,
 | 
					    BotConfiguration,
 | 
				
			||||||
    CollaboratorsPage,
 | 
					    CollaboratorsPage,
 | 
				
			||||||
    ConfigurationPage,
 | 
					    ConfigurationPage,
 | 
				
			||||||
 | 
					    CustomerSatisfactionPage,
 | 
				
			||||||
    FacebookReauthorize,
 | 
					    FacebookReauthorize,
 | 
				
			||||||
    GreetingsEditor,
 | 
					    GreetingsEditor,
 | 
				
			||||||
    PreChatFormSettings,
 | 
					    PreChatFormSettings,
 | 
				
			||||||
@@ -53,7 +55,6 @@ export default {
 | 
				
			|||||||
      greetingEnabled: true,
 | 
					      greetingEnabled: true,
 | 
				
			||||||
      greetingMessage: '',
 | 
					      greetingMessage: '',
 | 
				
			||||||
      emailCollectEnabled: false,
 | 
					      emailCollectEnabled: false,
 | 
				
			||||||
      csatSurveyEnabled: false,
 | 
					 | 
				
			||||||
      senderNameType: 'friendly',
 | 
					      senderNameType: 'friendly',
 | 
				
			||||||
      businessName: '',
 | 
					      businessName: '',
 | 
				
			||||||
      locktoSingleConversation: false,
 | 
					      locktoSingleConversation: false,
 | 
				
			||||||
@@ -107,6 +108,10 @@ export default {
 | 
				
			|||||||
          key: 'businesshours',
 | 
					          key: 'businesshours',
 | 
				
			||||||
          name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'),
 | 
					          name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'),
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          key: 'csat',
 | 
				
			||||||
 | 
					          name: this.$t('INBOX_MGMT.TABS.CSAT'),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
      ];
 | 
					      ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (this.isAWebWidgetInbox) {
 | 
					      if (this.isAWebWidgetInbox) {
 | 
				
			||||||
@@ -277,7 +282,6 @@ export default {
 | 
				
			|||||||
        this.greetingEnabled = this.inbox.greeting_enabled || false;
 | 
					        this.greetingEnabled = this.inbox.greeting_enabled || false;
 | 
				
			||||||
        this.greetingMessage = this.inbox.greeting_message || '';
 | 
					        this.greetingMessage = this.inbox.greeting_message || '';
 | 
				
			||||||
        this.emailCollectEnabled = this.inbox.enable_email_collect;
 | 
					        this.emailCollectEnabled = this.inbox.enable_email_collect;
 | 
				
			||||||
        this.csatSurveyEnabled = this.inbox.csat_survey_enabled;
 | 
					 | 
				
			||||||
        this.senderNameType = this.inbox.sender_name_type;
 | 
					        this.senderNameType = this.inbox.sender_name_type;
 | 
				
			||||||
        this.businessName = this.inbox.business_name;
 | 
					        this.businessName = this.inbox.business_name;
 | 
				
			||||||
        this.allowMessagesAfterResolved =
 | 
					        this.allowMessagesAfterResolved =
 | 
				
			||||||
@@ -300,7 +304,6 @@ export default {
 | 
				
			|||||||
          id: this.currentInboxId,
 | 
					          id: this.currentInboxId,
 | 
				
			||||||
          name: this.selectedInboxName,
 | 
					          name: this.selectedInboxName,
 | 
				
			||||||
          enable_email_collect: this.emailCollectEnabled,
 | 
					          enable_email_collect: this.emailCollectEnabled,
 | 
				
			||||||
          csat_survey_enabled: this.csatSurveyEnabled,
 | 
					 | 
				
			||||||
          allow_messages_after_resolved: this.allowMessagesAfterResolved,
 | 
					          allow_messages_after_resolved: this.allowMessagesAfterResolved,
 | 
				
			||||||
          greeting_enabled: this.greetingEnabled,
 | 
					          greeting_enabled: this.greetingEnabled,
 | 
				
			||||||
          greeting_message: this.greetingMessage || '',
 | 
					          greeting_message: this.greetingMessage || '',
 | 
				
			||||||
@@ -589,21 +592,6 @@ export default {
 | 
				
			|||||||
            </p>
 | 
					            </p>
 | 
				
			||||||
          </label>
 | 
					          </label>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <label class="pb-4">
 | 
					 | 
				
			||||||
            {{ $t('INBOX_MGMT.SETTINGS_POPUP.ENABLE_CSAT') }}
 | 
					 | 
				
			||||||
            <select v-model="csatSurveyEnabled">
 | 
					 | 
				
			||||||
              <option :value="true">
 | 
					 | 
				
			||||||
                {{ $t('INBOX_MGMT.EDIT.ENABLE_CSAT.ENABLED') }}
 | 
					 | 
				
			||||||
              </option>
 | 
					 | 
				
			||||||
              <option :value="false">
 | 
					 | 
				
			||||||
                {{ $t('INBOX_MGMT.EDIT.ENABLE_CSAT.DISABLED') }}
 | 
					 | 
				
			||||||
              </option>
 | 
					 | 
				
			||||||
            </select>
 | 
					 | 
				
			||||||
            <p class="pb-1 text-sm not-italic text-n-slate-11">
 | 
					 | 
				
			||||||
              {{ $t('INBOX_MGMT.SETTINGS_POPUP.ENABLE_CSAT_SUB_TEXT') }}
 | 
					 | 
				
			||||||
            </p>
 | 
					 | 
				
			||||||
          </label>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <label v-if="isAWebWidgetInbox" class="pb-4">
 | 
					          <label v-if="isAWebWidgetInbox" class="pb-4">
 | 
				
			||||||
            {{ $t('INBOX_MGMT.SETTINGS_POPUP.ALLOW_MESSAGES_AFTER_RESOLVED') }}
 | 
					            {{ $t('INBOX_MGMT.SETTINGS_POPUP.ALLOW_MESSAGES_AFTER_RESOLVED') }}
 | 
				
			||||||
            <select v-model="allowMessagesAfterResolved">
 | 
					            <select v-model="allowMessagesAfterResolved">
 | 
				
			||||||
@@ -802,6 +790,9 @@ export default {
 | 
				
			|||||||
      <div v-if="selectedTabKey === 'configuration'">
 | 
					      <div v-if="selectedTabKey === 'configuration'">
 | 
				
			||||||
        <ConfigurationPage :inbox="inbox" />
 | 
					        <ConfigurationPage :inbox="inbox" />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div v-if="selectedTabKey === 'csat'">
 | 
				
			||||||
 | 
					        <CustomerSatisfactionPage :inbox="inbox" />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
      <div v-if="selectedTabKey === 'preChatForm'">
 | 
					      <div v-if="selectedTabKey === 'preChatForm'">
 | 
				
			||||||
        <PreChatFormSettings :inbox="inbox" />
 | 
					        <PreChatFormSettings :inbox="inbox" />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,233 @@
 | 
				
			|||||||
 | 
					<script setup>
 | 
				
			||||||
 | 
					import { reactive, onMounted, ref, defineProps, watch, computed } from 'vue';
 | 
				
			||||||
 | 
					import { useI18n } from 'vue-i18n';
 | 
				
			||||||
 | 
					import { useAlert } from 'dashboard/composables';
 | 
				
			||||||
 | 
					import { useStore, useMapGetter } from 'dashboard/composables/store';
 | 
				
			||||||
 | 
					import { CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import WithLabel from 'v3/components/Form/WithLabel.vue';
 | 
				
			||||||
 | 
					import SectionLayout from 'dashboard/routes/dashboard/settings/account/components/SectionLayout.vue';
 | 
				
			||||||
 | 
					import CSATDisplayTypeSelector from './components/CSATDisplayTypeSelector.vue';
 | 
				
			||||||
 | 
					import Editor from 'dashboard/components-next/Editor/Editor.vue';
 | 
				
			||||||
 | 
					import FilterSelect from 'dashboard/components-next/filter/inputs/FilterSelect.vue';
 | 
				
			||||||
 | 
					import NextButton from 'dashboard/components-next/button/Button.vue';
 | 
				
			||||||
 | 
					import Switch from 'next/switch/Switch.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					  inbox: { type: Object, required: true },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { t } = useI18n();
 | 
				
			||||||
 | 
					const store = useStore();
 | 
				
			||||||
 | 
					const labels = useMapGetter('labels/getLabels');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isUpdating = ref(false);
 | 
				
			||||||
 | 
					const selectedLabelValues = ref([]);
 | 
				
			||||||
 | 
					const currentLabel = ref('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = reactive({
 | 
				
			||||||
 | 
					  csatSurveyEnabled: false,
 | 
				
			||||||
 | 
					  displayType: 'emoji',
 | 
				
			||||||
 | 
					  message: '',
 | 
				
			||||||
 | 
					  surveyRuleOperator: 'contains',
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const filterTypes = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    label: t('INBOX_MGMT.CSAT.SURVEY_RULE.OPERATOR.CONTAINS'),
 | 
				
			||||||
 | 
					    value: 'contains',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    label: t('INBOX_MGMT.CSAT.SURVEY_RULE.OPERATOR.DOES_NOT_CONTAINS'),
 | 
				
			||||||
 | 
					    value: 'does_not_contain',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const labelOptions = computed(() =>
 | 
				
			||||||
 | 
					  labels.value?.length
 | 
				
			||||||
 | 
					    ? labels.value
 | 
				
			||||||
 | 
					        .map(label => ({ label: label.title, value: label.title }))
 | 
				
			||||||
 | 
					        .filter(label => !selectedLabelValues.value.includes(label.value))
 | 
				
			||||||
 | 
					    : []
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const initializeState = () => {
 | 
				
			||||||
 | 
					  if (!props.inbox) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { csat_survey_enabled, csat_config } = props.inbox;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  state.csatSurveyEnabled = csat_survey_enabled || false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!csat_config) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    display_type: displayType = CSAT_DISPLAY_TYPES.EMOJI,
 | 
				
			||||||
 | 
					    message = '',
 | 
				
			||||||
 | 
					    survey_rules: surveyRules = {},
 | 
				
			||||||
 | 
					  } = csat_config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  state.displayType = displayType;
 | 
				
			||||||
 | 
					  state.message = message;
 | 
				
			||||||
 | 
					  state.surveyRuleOperator = surveyRules.operator || 'contains';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  selectedLabelValues.value = Array.isArray(surveyRules.values)
 | 
				
			||||||
 | 
					    ? [...surveyRules.values]
 | 
				
			||||||
 | 
					    : [];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					  initializeState();
 | 
				
			||||||
 | 
					  if (!labels.value?.length) store.dispatch('labels/get');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(() => props.inbox, initializeState, { immediate: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleLabelSelect = value => {
 | 
				
			||||||
 | 
					  if (!value || selectedLabelValues.value.includes(value)) {
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  selectedLabelValues.value.push(value);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const updateDisplayType = type => {
 | 
				
			||||||
 | 
					  state.displayType = type;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const updateSurveyRuleOperator = operator => {
 | 
				
			||||||
 | 
					  state.surveyRuleOperator = operator;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const removeLabel = label => {
 | 
				
			||||||
 | 
					  const index = selectedLabelValues.value.indexOf(label);
 | 
				
			||||||
 | 
					  if (index !== -1) {
 | 
				
			||||||
 | 
					    selectedLabelValues.value.splice(index, 1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const updateInbox = async attributes => {
 | 
				
			||||||
 | 
					  const payload = {
 | 
				
			||||||
 | 
					    id: props.inbox.id,
 | 
				
			||||||
 | 
					    formData: false,
 | 
				
			||||||
 | 
					    ...attributes,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return store.dispatch('inboxes/updateInbox', payload);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const saveSettings = async () => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    isUpdating.value = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const csatConfig = {
 | 
				
			||||||
 | 
					      display_type: state.displayType,
 | 
				
			||||||
 | 
					      message: state.message,
 | 
				
			||||||
 | 
					      survey_rules: {
 | 
				
			||||||
 | 
					        operator: state.surveyRuleOperator,
 | 
				
			||||||
 | 
					        values: selectedLabelValues.value,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await updateInbox({
 | 
				
			||||||
 | 
					      csat_survey_enabled: state.csatSurveyEnabled,
 | 
				
			||||||
 | 
					      csat_config: csatConfig,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useAlert(t('INBOX_MGMT.CSAT.API.SUCCESS_MESSAGE'));
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    useAlert(t('INBOX_MGMT.CSAT.API.ERROR_MESSAGE'));
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    isUpdating.value = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="mx-8">
 | 
				
			||||||
 | 
					    <SectionLayout
 | 
				
			||||||
 | 
					      :title="$t('INBOX_MGMT.CSAT.TITLE')"
 | 
				
			||||||
 | 
					      :description="$t('INBOX_MGMT.CSAT.SUBTITLE')"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <template #headerActions>
 | 
				
			||||||
 | 
					        <div class="flex justify-end">
 | 
				
			||||||
 | 
					          <Switch v-model="state.csatSurveyEnabled" />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="grid gap-5">
 | 
				
			||||||
 | 
					        <WithLabel
 | 
				
			||||||
 | 
					          :label="$t('INBOX_MGMT.CSAT.DISPLAY_TYPE.LABEL')"
 | 
				
			||||||
 | 
					          name="display_type"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <CSATDisplayTypeSelector
 | 
				
			||||||
 | 
					            :selected-type="state.displayType"
 | 
				
			||||||
 | 
					            @update="updateDisplayType"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </WithLabel>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <WithLabel :label="$t('INBOX_MGMT.CSAT.MESSAGE.LABEL')" name="message">
 | 
				
			||||||
 | 
					          <Editor
 | 
				
			||||||
 | 
					            v-model="state.message"
 | 
				
			||||||
 | 
					            :placeholder="$t('INBOX_MGMT.CSAT.MESSAGE.PLACEHOLDER')"
 | 
				
			||||||
 | 
					            :max-length="200"
 | 
				
			||||||
 | 
					            class="w-full"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </WithLabel>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <WithLabel
 | 
				
			||||||
 | 
					          :label="$t('INBOX_MGMT.CSAT.SURVEY_RULE.LABEL')"
 | 
				
			||||||
 | 
					          name="survey_rule"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div class="mb-4">
 | 
				
			||||||
 | 
					            <span
 | 
				
			||||||
 | 
					              class="inline-flex flex-wrap items-center gap-1.5 text-sm text-n-slate-12"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {{ $t('INBOX_MGMT.CSAT.SURVEY_RULE.DESCRIPTION_PREFIX') }}
 | 
				
			||||||
 | 
					              <FilterSelect
 | 
				
			||||||
 | 
					                v-model="state.surveyRuleOperator"
 | 
				
			||||||
 | 
					                variant="faded"
 | 
				
			||||||
 | 
					                :options="filterTypes"
 | 
				
			||||||
 | 
					                class="inline-flex shrink-0"
 | 
				
			||||||
 | 
					                @update:model-value="updateSurveyRuleOperator"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              {{ $t('INBOX_MGMT.CSAT.SURVEY_RULE.DESCRIPTION_SUFFIX') }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <NextButton
 | 
				
			||||||
 | 
					                v-for="label in selectedLabelValues"
 | 
				
			||||||
 | 
					                :key="label"
 | 
				
			||||||
 | 
					                sm
 | 
				
			||||||
 | 
					                faded
 | 
				
			||||||
 | 
					                slate
 | 
				
			||||||
 | 
					                trailing-icon
 | 
				
			||||||
 | 
					                :label="label"
 | 
				
			||||||
 | 
					                icon="i-lucide-x"
 | 
				
			||||||
 | 
					                class="inline-flex shrink-0"
 | 
				
			||||||
 | 
					                @click="removeLabel(label)"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <FilterSelect
 | 
				
			||||||
 | 
					                v-model="currentLabel"
 | 
				
			||||||
 | 
					                :options="labelOptions"
 | 
				
			||||||
 | 
					                :label="$t('INBOX_MGMT.CSAT.SURVEY_RULE.SELECT_PLACEHOLDER')"
 | 
				
			||||||
 | 
					                hide-label
 | 
				
			||||||
 | 
					                variant="faded"
 | 
				
			||||||
 | 
					                class="inline-flex shrink-0"
 | 
				
			||||||
 | 
					                @update:model-value="handleLabelSelect"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </span>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </WithLabel>
 | 
				
			||||||
 | 
					        <p class="text-sm italic text-n-slate-11">
 | 
				
			||||||
 | 
					          {{ $t('INBOX_MGMT.CSAT.NOTE') }}
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <NextButton
 | 
				
			||||||
 | 
					            type="submit"
 | 
				
			||||||
 | 
					            :label="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
 | 
				
			||||||
 | 
					            :is-loading="isUpdating"
 | 
				
			||||||
 | 
					            @click="saveSettings"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </SectionLayout>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					<script setup>
 | 
				
			||||||
 | 
					import { CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
 | 
				
			||||||
 | 
					import CSATEmojiInput from './CSATEmojiInput.vue';
 | 
				
			||||||
 | 
					import CSATStarInput from './CSATStarInput.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					  selectedType: {
 | 
				
			||||||
 | 
					    type: String,
 | 
				
			||||||
 | 
					    default: CSAT_DISPLAY_TYPES.EMOJI,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					const emit = defineEmits(['update']);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="flex flex-wrap gap-6 mt-2">
 | 
				
			||||||
 | 
					    <CSATEmojiInput
 | 
				
			||||||
 | 
					      :selected="props.selectedType === CSAT_DISPLAY_TYPES.EMOJI"
 | 
				
			||||||
 | 
					      @update="emit('update', $event)"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <CSATStarInput
 | 
				
			||||||
 | 
					      :selected="props.selectedType === CSAT_DISPLAY_TYPES.STAR"
 | 
				
			||||||
 | 
					      @update="emit('update', $event)"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					<script setup>
 | 
				
			||||||
 | 
					import { ref, computed } from 'vue';
 | 
				
			||||||
 | 
					import { CSAT_RATINGS, CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					  selected: {
 | 
				
			||||||
 | 
					    type: Boolean,
 | 
				
			||||||
 | 
					    default: false,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits(['update']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const selectionClass = computed(() => {
 | 
				
			||||||
 | 
					  return props.selected
 | 
				
			||||||
 | 
					    ? 'outline-n-brand bg-n-brand/5'
 | 
				
			||||||
 | 
					    : 'outline-n-weak bg-n-alpha-black2';
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emojis = CSAT_RATINGS;
 | 
				
			||||||
 | 
					const selectedEmoji = ref(5);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <button
 | 
				
			||||||
 | 
					    class="flex items-center rounded-lg transition-all duration-500 cursor-pointer outline outline-1 px-4 py-2 gap-2 min-w-56"
 | 
				
			||||||
 | 
					    :class="selectionClass"
 | 
				
			||||||
 | 
					    @click="emit('update', CSAT_DISPLAY_TYPES.EMOJI)"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      v-for="emoji in emojis"
 | 
				
			||||||
 | 
					      :key="emoji.key"
 | 
				
			||||||
 | 
					      class="rounded-full p-1 transition-transform duration-150 focus:outline-none flex items-center flex-shrink-0"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <span
 | 
				
			||||||
 | 
					        class="text-2xl"
 | 
				
			||||||
 | 
					        :class="selectedEmoji === emoji.value ? '' : 'grayscale opacity-60'"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {{ emoji.emoji }}
 | 
				
			||||||
 | 
					      </span>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </button>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					<script setup>
 | 
				
			||||||
 | 
					import { CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
 | 
				
			||||||
 | 
					import { computed } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					  selected: {
 | 
				
			||||||
 | 
					    type: Boolean,
 | 
				
			||||||
 | 
					    default: false,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits(['update']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const selectionClass = computed(() => {
 | 
				
			||||||
 | 
					  return props.selected
 | 
				
			||||||
 | 
					    ? 'bg-n-brand/5 outline-n-brand'
 | 
				
			||||||
 | 
					    : 'bg-n-alpha-black2 outline-n-weak';
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <button
 | 
				
			||||||
 | 
					    class="flex items-center rounded-lg transition-all duration-300 cursor-pointer outline outline-1 px-4 py-2 gap-2 min-w-56"
 | 
				
			||||||
 | 
					    :class="selectionClass"
 | 
				
			||||||
 | 
					    @click="emit('update', CSAT_DISPLAY_TYPES.STAR)"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      v-for="n in 5"
 | 
				
			||||||
 | 
					      :key="'star-' + n"
 | 
				
			||||||
 | 
					      class="rounded-full p-1 transition-transform duration-150 focus:outline-none flex items-center flex-shrink-0"
 | 
				
			||||||
 | 
					      :aria-label="`Star ${n}`"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <i class="i-ri-star-fill text-n-amber-9 text-2xl" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </button>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
@@ -1,14 +1,16 @@
 | 
				
			|||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { mapGetters } from 'vuex';
 | 
					import { mapGetters } from 'vuex';
 | 
				
			||||||
import Spinner from 'shared/components/Spinner.vue';
 | 
					import Spinner from 'shared/components/Spinner.vue';
 | 
				
			||||||
import { CSAT_RATINGS } from 'shared/constants/messages';
 | 
					import { CSAT_RATINGS, CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
 | 
				
			||||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
 | 
					import FluentIcon from 'shared/components/FluentIcon/Index.vue';
 | 
				
			||||||
 | 
					import StarRating from 'shared/components/StarRating.vue';
 | 
				
			||||||
import { getContrastingTextColor } from '@chatwoot/utils';
 | 
					import { getContrastingTextColor } from '@chatwoot/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  components: {
 | 
					  components: {
 | 
				
			||||||
    Spinner,
 | 
					    Spinner,
 | 
				
			||||||
    FluentIcon,
 | 
					    FluentIcon,
 | 
				
			||||||
 | 
					    StarRating,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    messageContentAttributes: {
 | 
					    messageContentAttributes: {
 | 
				
			||||||
@@ -19,6 +21,14 @@ export default {
 | 
				
			|||||||
      type: Number,
 | 
					      type: Number,
 | 
				
			||||||
      required: true,
 | 
					      required: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    displayType: {
 | 
				
			||||||
 | 
					      type: String,
 | 
				
			||||||
 | 
					      default: CSAT_DISPLAY_TYPES.EMOJI,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    message: {
 | 
				
			||||||
 | 
					      type: String,
 | 
				
			||||||
 | 
					      default: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
@@ -47,7 +57,13 @@ export default {
 | 
				
			|||||||
    title() {
 | 
					    title() {
 | 
				
			||||||
      return this.isRatingSubmitted
 | 
					      return this.isRatingSubmitted
 | 
				
			||||||
        ? this.$t('CSAT.SUBMITTED_TITLE')
 | 
					        ? this.$t('CSAT.SUBMITTED_TITLE')
 | 
				
			||||||
        : this.$t('CSAT.TITLE');
 | 
					        : this.message || this.$t('CSAT.TITLE');
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    isEmojiType() {
 | 
				
			||||||
 | 
					      return this.displayType === CSAT_DISPLAY_TYPES.EMOJI;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    isStarType() {
 | 
				
			||||||
 | 
					      return this.displayType === CSAT_DISPLAY_TYPES.STAR;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -88,10 +104,15 @@ export default {
 | 
				
			|||||||
        this.isUpdating = false;
 | 
					        this.isUpdating = false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    selectRating(rating) {
 | 
					    selectRating(rating) {
 | 
				
			||||||
      this.selectedRating = rating.value;
 | 
					      this.selectedRating = rating.value;
 | 
				
			||||||
      this.onSubmit();
 | 
					      this.onSubmit();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    selectStarRating(value) {
 | 
				
			||||||
 | 
					      this.selectedRating = value;
 | 
				
			||||||
 | 
					      this.onSubmit();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
@@ -104,7 +125,7 @@ export default {
 | 
				
			|||||||
    <h6 class="text-n-slate-12 text-sm font-medium pt-5 px-2.5 text-center">
 | 
					    <h6 class="text-n-slate-12 text-sm font-medium pt-5 px-2.5 text-center">
 | 
				
			||||||
      {{ title }}
 | 
					      {{ title }}
 | 
				
			||||||
    </h6>
 | 
					    </h6>
 | 
				
			||||||
    <div class="ratings flex justify-around py-5 px-4">
 | 
					    <div v-if="isEmojiType" class="ratings flex justify-around py-5 px-4">
 | 
				
			||||||
      <button
 | 
					      <button
 | 
				
			||||||
        v-for="rating in ratings"
 | 
					        v-for="rating in ratings"
 | 
				
			||||||
        :key="rating.key"
 | 
					        :key="rating.key"
 | 
				
			||||||
@@ -114,6 +135,12 @@ export default {
 | 
				
			|||||||
        {{ rating.emoji }}
 | 
					        {{ rating.emoji }}
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					    <StarRating
 | 
				
			||||||
 | 
					      v-else-if="isStarType"
 | 
				
			||||||
 | 
					      :selected-rating="selectedRating"
 | 
				
			||||||
 | 
					      :is-disabled="isRatingSubmitted"
 | 
				
			||||||
 | 
					      @select-rating="selectStarRating"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
    <form
 | 
					    <form
 | 
				
			||||||
      v-if="!isFeedbackSubmitted"
 | 
					      v-if="!isFeedbackSubmitted"
 | 
				
			||||||
      class="feedback-form flex"
 | 
					      class="feedback-form flex"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										65
									
								
								app/javascript/shared/components/StarRating.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								app/javascript/shared/components/StarRating.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					<script setup>
 | 
				
			||||||
 | 
					import { ref, defineProps, defineEmits } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					  selectedRating: {
 | 
				
			||||||
 | 
					    type: Number,
 | 
				
			||||||
 | 
					    default: null,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  isDisabled: {
 | 
				
			||||||
 | 
					    type: Boolean,
 | 
				
			||||||
 | 
					    default: false,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits(['selectRating']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const starRatings = [1, 2, 3, 4, 5];
 | 
				
			||||||
 | 
					const hoveredRating = ref(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onHoverRating = value => {
 | 
				
			||||||
 | 
					  if (props.isDisabled) return;
 | 
				
			||||||
 | 
					  hoveredRating.value = value;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const selectRating = value => {
 | 
				
			||||||
 | 
					  if (props.isDisabled) return;
 | 
				
			||||||
 | 
					  emit('selectRating', value);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getStarClass = value => {
 | 
				
			||||||
 | 
					  const isStarActive =
 | 
				
			||||||
 | 
					    (hoveredRating.value > 0 &&
 | 
				
			||||||
 | 
					      !props.isDisabled &&
 | 
				
			||||||
 | 
					      hoveredRating.value >= value) ||
 | 
				
			||||||
 | 
					    props.selectedRating >= value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const starTypeClass = isStarActive
 | 
				
			||||||
 | 
					    ? 'i-ri-star-fill text-n-amber-9'
 | 
				
			||||||
 | 
					    : 'i-ri-star-line text-n-slate-10';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return starTypeClass;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="flex justify-center py-5 px-4 gap-3">
 | 
				
			||||||
 | 
					    <button
 | 
				
			||||||
 | 
					      v-for="value in starRatings"
 | 
				
			||||||
 | 
					      :key="value"
 | 
				
			||||||
 | 
					      type="button"
 | 
				
			||||||
 | 
					      class="rounded-full p-1 transition-all duration-200 focus:enabled:scale-[1.2] focus-within:enabled:scale-[1.2] hover:enabled:scale-[1.2] focus:outline-none flex items-center flex-shrink-0"
 | 
				
			||||||
 | 
					      :class="{ 'cursor-not-allowed opacity-50': isDisabled }"
 | 
				
			||||||
 | 
					      :disabled="isDisabled"
 | 
				
			||||||
 | 
					      :aria-label="'Star ' + value"
 | 
				
			||||||
 | 
					      @click="selectRating(value)"
 | 
				
			||||||
 | 
					      @mouseenter="onHoverRating(value)"
 | 
				
			||||||
 | 
					      @mouseleave="onHoverRating(0)"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <span
 | 
				
			||||||
 | 
					        :class="getStarClass(value)"
 | 
				
			||||||
 | 
					        class="transition-all duration-500 text-2xl"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
@@ -100,6 +100,11 @@ export const CSAT_RATINGS = [
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const CSAT_DISPLAY_TYPES = {
 | 
				
			||||||
 | 
					  EMOJI: 'emoji',
 | 
				
			||||||
 | 
					  STAR: 'star',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const AUDIO_FORMATS = {
 | 
					export const AUDIO_FORMATS = {
 | 
				
			||||||
  WEBM: 'audio/webm',
 | 
					  WEBM: 'audio/webm',
 | 
				
			||||||
  OGG: 'audio/ogg',
 | 
					  OGG: 'audio/ogg',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ export default {
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div id="app" class="woot-survey-wrap min-h-screen">
 | 
					  <div id="app" dir="ltr" class="woot-survey-wrap min-h-screen">
 | 
				
			||||||
    <Response />
 | 
					    <Response />
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@
 | 
				
			|||||||
@import 'tailwindcss/utilities';
 | 
					@import 'tailwindcss/utilities';
 | 
				
			||||||
@import 'widget/assets/scss/reset';
 | 
					@import 'widget/assets/scss/reset';
 | 
				
			||||||
@import 'shared/assets/fonts/widget_fonts';
 | 
					@import 'shared/assets/fonts/widget_fonts';
 | 
				
			||||||
 | 
					@import 'dashboard/assets/scss/next-colors';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
html,
 | 
					html,
 | 
				
			||||||
body {
 | 
					body {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,7 @@ export default {
 | 
				
			|||||||
      class="ion-checkmark-circled text-3xl text-green-500 mr-1"
 | 
					      class="ion-checkmark-circled text-3xl text-green-500 mr-1"
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
    <i v-if="showError" class="ion-android-alert text-3xl text-red-600 mr-1" />
 | 
					    <i v-if="showError" class="ion-android-alert text-3xl text-red-600 mr-1" />
 | 
				
			||||||
    <label class="text-base font-medium text-black-800 mt-4 mb-4">
 | 
					    <label class="text-base font-medium text-n-slate-12 mt-4 mb-4">
 | 
				
			||||||
      {{ message }}
 | 
					      {{ message }}
 | 
				
			||||||
    </label>
 | 
					    </label>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,7 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="mt-6">
 | 
					  <div class="mt-6">
 | 
				
			||||||
    <label class="text-base font-medium text-black-800">
 | 
					    <label class="text-base font-medium text-n-slate-12">
 | 
				
			||||||
      {{ $t('SURVEY.FEEDBACK.LABEL') }}
 | 
					      {{ $t('SURVEY.FEEDBACK.LABEL') }}
 | 
				
			||||||
    </label>
 | 
					    </label>
 | 
				
			||||||
    <TextArea
 | 
					    <TextArea
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,8 +5,11 @@ import Spinner from 'shared/components/Spinner.vue';
 | 
				
			|||||||
import Rating from 'survey/components/Rating.vue';
 | 
					import Rating from 'survey/components/Rating.vue';
 | 
				
			||||||
import Feedback from 'survey/components/Feedback.vue';
 | 
					import Feedback from 'survey/components/Feedback.vue';
 | 
				
			||||||
import Banner from 'survey/components/Banner.vue';
 | 
					import Banner from 'survey/components/Banner.vue';
 | 
				
			||||||
 | 
					import StarRating from 'shared/components/StarRating.vue';
 | 
				
			||||||
import { getSurveyDetails, updateSurvey } from 'survey/api/survey';
 | 
					import { getSurveyDetails, updateSurvey } from 'survey/api/survey';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: 'Response',
 | 
					  name: 'Response',
 | 
				
			||||||
  components: {
 | 
					  components: {
 | 
				
			||||||
@@ -15,6 +18,7 @@ export default {
 | 
				
			|||||||
    Spinner,
 | 
					    Spinner,
 | 
				
			||||||
    Banner,
 | 
					    Banner,
 | 
				
			||||||
    Feedback,
 | 
					    Feedback,
 | 
				
			||||||
 | 
					    StarRating,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
@@ -26,6 +30,8 @@ export default {
 | 
				
			|||||||
      isUpdating: false,
 | 
					      isUpdating: false,
 | 
				
			||||||
      logo: '',
 | 
					      logo: '',
 | 
				
			||||||
      inboxName: '',
 | 
					      inboxName: '',
 | 
				
			||||||
 | 
					      displayType: CSAT_DISPLAY_TYPES.EMOJI,
 | 
				
			||||||
 | 
					      messageContent: '',
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
@@ -42,16 +48,22 @@ export default {
 | 
				
			|||||||
    isButtonDisabled() {
 | 
					    isButtonDisabled() {
 | 
				
			||||||
      return !(this.selectedRating && this.feedback);
 | 
					      return !(this.selectedRating && this.feedback);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    isEmojiType() {
 | 
				
			||||||
 | 
					      return this.displayType === CSAT_DISPLAY_TYPES.EMOJI;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    isStarType() {
 | 
				
			||||||
 | 
					      return this.displayType === CSAT_DISPLAY_TYPES.STAR;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    shouldShowBanner() {
 | 
					    shouldShowBanner() {
 | 
				
			||||||
      return this.isRatingSubmitted || this.errorMessage;
 | 
					      return this.isRatingSubmitted || this.errorMessage;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    enableFeedbackForm() {
 | 
					    enableFeedbackForm() {
 | 
				
			||||||
      return !this.isFeedbackSubmitted && this.isRatingSubmitted;
 | 
					      return !this.isFeedbackSubmitted && this.isRatingSubmitted;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    shouldShowErrorMesage() {
 | 
					    shouldShowErrorMessage() {
 | 
				
			||||||
      return !!this.errorMessage;
 | 
					      return !!this.errorMessage;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    shouldShowSuccessMesage() {
 | 
					    shouldShowSuccessMessage() {
 | 
				
			||||||
      return !!this.isRatingSubmitted;
 | 
					      return !!this.isRatingSubmitted;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    message() {
 | 
					    message() {
 | 
				
			||||||
@@ -82,6 +94,10 @@ export default {
 | 
				
			|||||||
        this.surveyDetails = result?.data?.csat_survey_response;
 | 
					        this.surveyDetails = result?.data?.csat_survey_response;
 | 
				
			||||||
        this.selectedRating = this.surveyDetails?.rating;
 | 
					        this.selectedRating = this.surveyDetails?.rating;
 | 
				
			||||||
        this.feedbackMessage = this.surveyDetails?.feedback_message || '';
 | 
					        this.feedbackMessage = this.surveyDetails?.feedback_message || '';
 | 
				
			||||||
 | 
					        this.displayType = result.data.display_type || CSAT_DISPLAY_TYPES.EMOJI;
 | 
				
			||||||
 | 
					        this.messageContent =
 | 
				
			||||||
 | 
					          result.data.content ||
 | 
				
			||||||
 | 
					          this.$t('SURVEY.DESCRIPTION', { inboxName: this.inboxName });
 | 
				
			||||||
        this.setLocale(result.data.locale);
 | 
					        this.setLocale(result.data.locale);
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        const errorMessage = error?.response?.data?.message;
 | 
					        const errorMessage = error?.response?.data?.message;
 | 
				
			||||||
@@ -129,41 +145,49 @@ export default {
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div
 | 
					  <div
 | 
				
			||||||
    v-if="isLoading"
 | 
					    v-if="isLoading"
 | 
				
			||||||
    class="flex items-center justify-center flex-1 h-full min-h-screen bg-black-25"
 | 
					    class="flex items-center justify-center flex-1 h-full min-h-screen bg-n-background"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <Spinner size="" />
 | 
					    <Spinner size="" />
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  <div
 | 
					  <div
 | 
				
			||||||
    v-else
 | 
					    v-else
 | 
				
			||||||
    class="flex items-center justify-center w-full h-full min-h-screen overflow-auto bg-slate-50"
 | 
					    class="flex items-center justify-center w-full h-full min-h-screen overflow-auto bg-n-background"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      class="flex flex-col w-full h-full bg-white rounded-lg shadow-lg lg:w-2/5 lg:h-auto"
 | 
					      class="flex flex-col w-full h-full bg-n-solid-1 rounded-lg border border-solid border-n-weak shadow-md lg:w-2/5 lg:h-auto"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <div class="w-full px-12 pt-12 pb-6 m-auto my-0">
 | 
					      <div class="w-full px-12 pt-12 pb-6 m-auto my-0">
 | 
				
			||||||
        <img v-if="logo" :src="logo" alt="Chatwoot logo" class="mb-6 logo" />
 | 
					        <img v-if="logo" :src="logo" alt="Chatwoot logo" class="mb-6 logo" />
 | 
				
			||||||
        <p
 | 
					        <p
 | 
				
			||||||
          v-if="!isRatingSubmitted"
 | 
					          v-if="!isRatingSubmitted"
 | 
				
			||||||
          class="mb-8 text-lg leading-relaxed text-black-700"
 | 
					          class="mb-8 text-lg leading-relaxed text-n-slate-12"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          {{ $t('SURVEY.DESCRIPTION', { inboxName }) }}
 | 
					          {{ messageContent }}
 | 
				
			||||||
        </p>
 | 
					        </p>
 | 
				
			||||||
        <Banner
 | 
					        <Banner
 | 
				
			||||||
          v-if="shouldShowBanner"
 | 
					          v-if="shouldShowBanner"
 | 
				
			||||||
          :show-success="shouldShowSuccessMesage"
 | 
					          :show-success="shouldShowSuccessMessage"
 | 
				
			||||||
          :show-error="shouldShowErrorMesage"
 | 
					          :show-error="shouldShowErrorMessage"
 | 
				
			||||||
          :message="message"
 | 
					          :message="message"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <label
 | 
					        <label
 | 
				
			||||||
          v-if="!isRatingSubmitted"
 | 
					          v-if="!isRatingSubmitted"
 | 
				
			||||||
          class="mb-4 text-base font-medium text-black-800"
 | 
					          class="mb-4 text-base font-medium text-n-slate-11"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          {{ $t('SURVEY.RATING.LABEL') }}
 | 
					          {{ $t('SURVEY.RATING.LABEL') }}
 | 
				
			||||||
        </label>
 | 
					        </label>
 | 
				
			||||||
        <Rating
 | 
					        <Rating
 | 
				
			||||||
 | 
					          v-if="isEmojiType"
 | 
				
			||||||
          :selected-rating="selectedRating"
 | 
					          :selected-rating="selectedRating"
 | 
				
			||||||
          @select-rating="selectRating"
 | 
					          @select-rating="selectRating"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
					        <StarRating
 | 
				
			||||||
 | 
					          v-if="isStarType"
 | 
				
			||||||
 | 
					          :selected-rating="selectedRating"
 | 
				
			||||||
 | 
					          :is-disabled="isRatingSubmitted"
 | 
				
			||||||
 | 
					          class="[&>button>span]:text-4xl !justify-start !px-0"
 | 
				
			||||||
 | 
					          @select-rating="selectRating"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
        <Feedback
 | 
					        <Feedback
 | 
				
			||||||
          v-if="enableFeedbackForm"
 | 
					          v-if="enableFeedbackForm"
 | 
				
			||||||
          :is-updating="isUpdating"
 | 
					          :is-updating="isUpdating"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -144,6 +144,8 @@ export default {
 | 
				
			|||||||
    <CustomerSatisfaction
 | 
					    <CustomerSatisfaction
 | 
				
			||||||
      v-if="isCSAT"
 | 
					      v-if="isCSAT"
 | 
				
			||||||
      :message-content-attributes="messageContentAttributes.submitted_values"
 | 
					      :message-content-attributes="messageContentAttributes.submitted_values"
 | 
				
			||||||
 | 
					      :display-type="messageContentAttributes.display_type"
 | 
				
			||||||
 | 
					      :message="message"
 | 
				
			||||||
      :message-id="messageId"
 | 
					      :message-id="messageId"
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@
 | 
				
			|||||||
#  auto_assignment_config        :jsonb
 | 
					#  auto_assignment_config        :jsonb
 | 
				
			||||||
#  business_name                 :string
 | 
					#  business_name                 :string
 | 
				
			||||||
#  channel_type                  :string
 | 
					#  channel_type                  :string
 | 
				
			||||||
 | 
					#  csat_config                   :jsonb            not null
 | 
				
			||||||
#  csat_survey_enabled           :boolean          default(FALSE)
 | 
					#  csat_survey_enabled           :boolean          default(FALSE)
 | 
				
			||||||
#  email_address                 :string
 | 
					#  email_address                 :string
 | 
				
			||||||
#  enable_auto_assignment        :boolean          default(TRUE)
 | 
					#  enable_auto_assignment        :boolean          default(TRUE)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,8 @@ class MessageTemplates::Template::CsatSurvey
 | 
				
			|||||||
  pattr_initialize [:conversation!]
 | 
					  pattr_initialize [:conversation!]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform
 | 
					  def perform
 | 
				
			||||||
 | 
					    return unless should_send_csat_survey?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ActiveRecord::Base.transaction do
 | 
					    ActiveRecord::Base.transaction do
 | 
				
			||||||
      conversation.messages.create!(csat_survey_message_params)
 | 
					      conversation.messages.create!(csat_survey_message_params)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
@@ -9,8 +11,47 @@ class MessageTemplates::Template::CsatSurvey
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  delegate :contact, :account, to: :conversation
 | 
					  delegate :contact, :account, :inbox, to: :conversation
 | 
				
			||||||
  delegate :inbox, to: :message
 | 
					  delegate :csat_config, to: :inbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def should_send_csat_survey?
 | 
				
			||||||
 | 
					    return true unless survey_rules_configured?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    labels = conversation.label_list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return true if rule_values.empty?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    case rule_operator
 | 
				
			||||||
 | 
					    when 'contains'
 | 
				
			||||||
 | 
					      rule_values.any? { |label| labels.include?(label) }
 | 
				
			||||||
 | 
					    when 'does_not_contain'
 | 
				
			||||||
 | 
					      rule_values.none? { |label| labels.include?(label) }
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      true
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def survey_rules_configured?
 | 
				
			||||||
 | 
					    return false if csat_config.blank?
 | 
				
			||||||
 | 
					    return false if csat_config['survey_rules'].blank?
 | 
				
			||||||
 | 
					    return false if rule_values.empty?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    true
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def rule_operator
 | 
				
			||||||
 | 
					    csat_config.dig('survey_rules', 'operator') || 'contains'
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def rule_values
 | 
				
			||||||
 | 
					    csat_config.dig('survey_rules', 'values') || []
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def message_content
 | 
				
			||||||
 | 
					    return I18n.t('conversations.templates.csat_input_message_body') if csat_config.blank? || csat_config['message'].blank?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    csat_config['message']
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def csat_survey_message_params
 | 
					  def csat_survey_message_params
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -18,7 +59,18 @@ class MessageTemplates::Template::CsatSurvey
 | 
				
			|||||||
      inbox_id: @conversation.inbox_id,
 | 
					      inbox_id: @conversation.inbox_id,
 | 
				
			||||||
      message_type: :template,
 | 
					      message_type: :template,
 | 
				
			||||||
      content_type: :input_csat,
 | 
					      content_type: :input_csat,
 | 
				
			||||||
      content: I18n.t('conversations.templates.csat_input_message_body')
 | 
					      content: message_content,
 | 
				
			||||||
 | 
					      content_attributes: content_attributes
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def csat_config
 | 
				
			||||||
 | 
					    inbox.csat_config || {}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def content_attributes
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      display_type: csat_config['display_type'] || 'emoji'
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ json.greeting_message resource.greeting_message
 | 
				
			|||||||
json.working_hours_enabled resource.working_hours_enabled
 | 
					json.working_hours_enabled resource.working_hours_enabled
 | 
				
			||||||
json.enable_email_collect resource.enable_email_collect
 | 
					json.enable_email_collect resource.enable_email_collect
 | 
				
			||||||
json.csat_survey_enabled resource.csat_survey_enabled
 | 
					json.csat_survey_enabled resource.csat_survey_enabled
 | 
				
			||||||
 | 
					json.csat_config resource.csat_config
 | 
				
			||||||
json.enable_auto_assignment resource.enable_auto_assignment
 | 
					json.enable_auto_assignment resource.enable_auto_assignment
 | 
				
			||||||
json.auto_assignment_config resource.auto_assignment_config
 | 
					json.auto_assignment_config resource.auto_assignment_config
 | 
				
			||||||
json.out_of_office_message resource.out_of_office_message
 | 
					json.out_of_office_message resource.out_of_office_message
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,7 @@
 | 
				
			|||||||
json.id resource.id
 | 
					json.id resource.id
 | 
				
			||||||
json.csat_survey_response resource.csat_survey_response
 | 
					json.csat_survey_response resource.csat_survey_response
 | 
				
			||||||
 | 
					json.display_type resource.inbox.csat_config.try(:[], 'display_type') || 'emoji'
 | 
				
			||||||
 | 
					json.content resource.inbox.csat_config.try(:[], 'message')
 | 
				
			||||||
json.inbox_avatar_url resource.inbox.avatar_url
 | 
					json.inbox_avatar_url resource.inbox.avatar_url
 | 
				
			||||||
json.inbox_name resource.inbox.name
 | 
					json.inbox_name resource.inbox.name
 | 
				
			||||||
json.locale resource.account.locale
 | 
					json.locale resource.account.locale
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								db/migrate/20250514045638_add_csat_config_to_inboxes.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrate/20250514045638_add_csat_config_to_inboxes.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					class AddCsatConfigToInboxes < ActiveRecord::Migration[7.0]
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    add_column :inboxes, :csat_config, :jsonb, default: {}, null: false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -10,7 +10,7 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
# It's strongly recommended that you check this file into your version control system.
 | 
					# It's strongly recommended that you check this file into your version control system.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ActiveRecord::Schema[7.0].define(version: 2025_05_12_231037) do
 | 
					ActiveRecord::Schema[7.0].define(version: 2025_05_14_045638) do
 | 
				
			||||||
  # These extensions should be enabled to support this database
 | 
					  # These extensions should be enabled to support this database
 | 
				
			||||||
  enable_extension "pg_stat_statements"
 | 
					  enable_extension "pg_stat_statements"
 | 
				
			||||||
  enable_extension "pg_trgm"
 | 
					  enable_extension "pg_trgm"
 | 
				
			||||||
@@ -729,6 +729,7 @@ ActiveRecord::Schema[7.0].define(version: 2025_05_12_231037) do
 | 
				
			|||||||
    t.bigint "portal_id"
 | 
					    t.bigint "portal_id"
 | 
				
			||||||
    t.integer "sender_name_type", default: 0, null: false
 | 
					    t.integer "sender_name_type", default: 0, null: false
 | 
				
			||||||
    t.string "business_name"
 | 
					    t.string "business_name"
 | 
				
			||||||
 | 
					    t.jsonb "csat_config", default: {}, null: false
 | 
				
			||||||
    t.index ["account_id"], name: "index_inboxes_on_account_id"
 | 
					    t.index ["account_id"], name: "index_inboxes_on_account_id"
 | 
				
			||||||
    t.index ["channel_id", "channel_type"], name: "index_inboxes_on_channel_id_and_channel_type"
 | 
					    t.index ["channel_id", "channel_type"], name: "index_inboxes_on_channel_id_and_channel_type"
 | 
				
			||||||
    t.index ["portal_id"], name: "index_inboxes_on_portal_id"
 | 
					    t.index ["portal_id"], name: "index_inboxes_on_portal_id"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -717,6 +717,94 @@ RSpec.describe 'Inboxes API', type: :request do
 | 
				
			|||||||
        expect(email_channel.reload.smtp_authentication).to eq('plain')
 | 
					        expect(email_channel.reload.smtp_authentication).to eq('plain')
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when handling CSAT configuration' do
 | 
				
			||||||
 | 
					      let(:admin) { create(:user, account: account, role: :administrator) }
 | 
				
			||||||
 | 
					      let(:inbox) { create(:inbox, account: account) }
 | 
				
			||||||
 | 
					      let(:csat_config) do
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          'display_type' => 'emoji',
 | 
				
			||||||
 | 
					          'message' => 'How would you rate your experience?',
 | 
				
			||||||
 | 
					          'survey_rules' => {
 | 
				
			||||||
 | 
					            'operator' => 'contains',
 | 
				
			||||||
 | 
					            'values' => %w[support help]
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'successfully updates the inbox with CSAT configuration' do
 | 
				
			||||||
 | 
					        patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
 | 
				
			||||||
 | 
					              params: {
 | 
				
			||||||
 | 
					                csat_survey_enabled: true,
 | 
				
			||||||
 | 
					                csat_config: csat_config
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              headers: admin.create_new_auth_token,
 | 
				
			||||||
 | 
					              as: :json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'when CSAT is configured' do
 | 
				
			||||||
 | 
					        before do
 | 
				
			||||||
 | 
					          patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
 | 
				
			||||||
 | 
					                params: {
 | 
				
			||||||
 | 
					                  csat_survey_enabled: true,
 | 
				
			||||||
 | 
					                  csat_config: csat_config
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                headers: admin.create_new_auth_token,
 | 
				
			||||||
 | 
					                as: :json
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'returns configured CSAT settings in inbox details' do
 | 
				
			||||||
 | 
					          get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
 | 
				
			||||||
 | 
					              headers: admin.create_new_auth_token,
 | 
				
			||||||
 | 
					              as: :json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					          json_response = response.parsed_body
 | 
				
			||||||
 | 
					          expect(json_response['csat_survey_enabled']).to be true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          saved_config = json_response['csat_config']
 | 
				
			||||||
 | 
					          expect(saved_config).to be_present
 | 
				
			||||||
 | 
					          expect(saved_config['display_type']).to eq('emoji')
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'returns configured CSAT message' do
 | 
				
			||||||
 | 
					          get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
 | 
				
			||||||
 | 
					              headers: admin.create_new_auth_token,
 | 
				
			||||||
 | 
					              as: :json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          json_response = response.parsed_body
 | 
				
			||||||
 | 
					          saved_config = json_response['csat_config']
 | 
				
			||||||
 | 
					          expect(saved_config['message']).to eq('How would you rate your experience?')
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'returns configured CSAT survey rules' do
 | 
				
			||||||
 | 
					          get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
 | 
				
			||||||
 | 
					              headers: admin.create_new_auth_token,
 | 
				
			||||||
 | 
					              as: :json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          json_response = response.parsed_body
 | 
				
			||||||
 | 
					          saved_config = json_response['csat_config']
 | 
				
			||||||
 | 
					          expect(saved_config['survey_rules']['operator']).to eq('contains')
 | 
				
			||||||
 | 
					          expect(saved_config['survey_rules']['values']).to match_array(%w[support help])
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'includes CSAT configuration in inbox list' do
 | 
				
			||||||
 | 
					          get "/api/v1/accounts/#{account.id}/inboxes",
 | 
				
			||||||
 | 
					              headers: admin.create_new_auth_token,
 | 
				
			||||||
 | 
					              as: :json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					          inbox_list = response.parsed_body
 | 
				
			||||||
 | 
					          found_inbox = inbox_list['payload'].find { |i| i['id'] == inbox.id }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(found_inbox['csat_survey_enabled']).to be true
 | 
				
			||||||
 | 
					          expect(found_inbox['csat_config']).to be_present
 | 
				
			||||||
 | 
					          expect(found_inbox['csat_config']['display_type']).to eq('emoji')
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe 'GET /api/v1/accounts/{account.id}/inboxes/{inbox.id}/agent_bot' do
 | 
					  describe 'GET /api/v1/accounts/{account.id}/inboxes/{inbox.id}/agent_bot' do
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,100 @@
 | 
				
			|||||||
require 'rails_helper'
 | 
					require 'rails_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe MessageTemplates::Template::CsatSurvey do
 | 
					describe MessageTemplates::Template::CsatSurvey do
 | 
				
			||||||
  context 'when this hook is called' do
 | 
					  let(:account) { create(:account) }
 | 
				
			||||||
    let(:conversation) { create(:conversation) }
 | 
					  let(:inbox) { create(:inbox, account: account) }
 | 
				
			||||||
 | 
					  let(:conversation) { create(:conversation, account: account, inbox: inbox) }
 | 
				
			||||||
 | 
					  let(:service) { described_class.new(conversation: conversation) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it 'creates the out of office messages' do
 | 
					  describe '#perform' do
 | 
				
			||||||
      described_class.new(conversation: conversation).perform
 | 
					    context 'when no survey rules are configured' do
 | 
				
			||||||
      expect(conversation.messages.template.count).to eq(1)
 | 
					      it 'creates a CSAT survey message' do
 | 
				
			||||||
      expect(conversation.messages.template.first.content_type).to eq('input_csat')
 | 
					        inbox.update(csat_config: {})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        service.perform
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(conversation.messages.template.count).to eq(1)
 | 
				
			||||||
 | 
					        expect(conversation.messages.template.first.content_type).to eq('input_csat')
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#perform with contains operator' do
 | 
				
			||||||
 | 
					    let(:csat_config) do
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        'display_type' => 'emoji',
 | 
				
			||||||
 | 
					        'message' => 'Please rate your experience',
 | 
				
			||||||
 | 
					        'survey_rules' => {
 | 
				
			||||||
 | 
					          'operator' => 'contains',
 | 
				
			||||||
 | 
					          'values' => %w[support help]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before do
 | 
				
			||||||
 | 
					      inbox.update(csat_config: csat_config)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when conversation has matching labels' do
 | 
				
			||||||
 | 
					      it 'creates a CSAT survey message' do
 | 
				
			||||||
 | 
					        conversation.update(label_list: %w[support urgent])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        service.perform
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(conversation.messages.template.count).to eq(1)
 | 
				
			||||||
 | 
					        message = conversation.messages.template.first
 | 
				
			||||||
 | 
					        expect(message.content_type).to eq('input_csat')
 | 
				
			||||||
 | 
					        expect(message.content).to eq('Please rate your experience')
 | 
				
			||||||
 | 
					        expect(message.content_attributes['display_type']).to eq('emoji')
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when conversation has no matching labels' do
 | 
				
			||||||
 | 
					      it 'does not create a CSAT survey message' do
 | 
				
			||||||
 | 
					        conversation.update(label_list: %w[billing-support payment])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        service.perform
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(conversation.messages.template.count).to eq(0)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#perform with does_not_contain operator' do
 | 
				
			||||||
 | 
					    let(:csat_config) do
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        'display_type' => 'emoji',
 | 
				
			||||||
 | 
					        'message' => 'Please rate your experience',
 | 
				
			||||||
 | 
					        'survey_rules' => {
 | 
				
			||||||
 | 
					          'operator' => 'does_not_contain',
 | 
				
			||||||
 | 
					          'values' => %w[support help]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before do
 | 
				
			||||||
 | 
					      inbox.update(csat_config: csat_config)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when conversation does not have matching labels' do
 | 
				
			||||||
 | 
					      it 'creates a CSAT survey message' do
 | 
				
			||||||
 | 
					        conversation.update(label_list: %w[billing payment])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        service.perform
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(conversation.messages.template.count).to eq(1)
 | 
				
			||||||
 | 
					        expect(conversation.messages.template.first.content_type).to eq('input_csat')
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when conversation has matching labels' do
 | 
				
			||||||
 | 
					      it 'does not create a CSAT survey message' do
 | 
				
			||||||
 | 
					        conversation.update(label_list: %w[support urgent])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        service.perform
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(conversation.messages.template.count).to eq(0)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user