diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 00e718614..b94fc0d90 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -1,6 +1,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController include Events::Types - before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes] + before_action :render_not_found_if_empty, + only: [:toggle_typing, :toggle_status, :request_handoff, :set_custom_attributes, :destroy_custom_attributes] def index @conversation = conversation @@ -64,6 +65,11 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController head :ok end + def request_handoff + perform_handoff if conversation.pending? + head :ok + end + def set_custom_attributes conversation.update!(custom_attributes: permitted_params[:custom_attributes]) end @@ -99,4 +105,40 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController message: [:content, :referer_url, :timestamp, :echo_id], custom_attributes: {}) end + + def perform_handoff + assistant = captain_assistant + previous_executed_by = Current.executed_by + Current.executed_by = assistant if assistant + + I18n.with_locale(conversation.account.locale) do + create_captain_handoff_message(assistant) if assistant + conversation.bot_handoff! + send_out_of_office_message_after_handoff + end + ensure + Current.executed_by = previous_executed_by + end + + def captain_assistant + return unless inbox.respond_to?(:captain_assistant) + + inbox.captain_assistant + end + + def create_captain_handoff_message(assistant) + conversation.messages.create!( + message_type: :outgoing, + account_id: conversation.account_id, + inbox_id: conversation.inbox_id, + sender: assistant, + content: assistant.config['handoff_message'].presence || I18n.t('conversations.captain.handoff') + ) + end + + def send_out_of_office_message_after_handoff + return if conversation.campaign.present? + + ::MessageTemplates::Template::OutOfOffice.perform_if_applicable(conversation) + end end diff --git a/app/javascript/widget/api/conversation.js b/app/javascript/widget/api/conversation.js index 05e81ff9f..588f68b87 100755 --- a/app/javascript/widget/api/conversation.js +++ b/app/javascript/widget/api/conversation.js @@ -62,6 +62,12 @@ const toggleStatus = async () => { ); }; +const requestHandoff = async () => { + return API.post( + `/api/v1/widget/conversations/request_handoff${window.location.search}` + ); +}; + const setCustomAttributes = async customAttributes => { return API.post( `/api/v1/widget/conversations/set_custom_attributes${window.location.search}`, @@ -90,6 +96,7 @@ export { setUserLastSeenAt, sendEmailTranscript, toggleStatus, + requestHandoff, setCustomAttributes, deleteCustomAttribute, }; diff --git a/app/javascript/widget/components/ChatFooter.vue b/app/javascript/widget/components/ChatFooter.vue index c85727a2b..aa8fd4aa4 100755 --- a/app/javascript/widget/components/ChatFooter.vue +++ b/app/javascript/widget/components/ChatFooter.vue @@ -5,12 +5,15 @@ import CustomButton from 'shared/components/Button.vue'; import FooterReplyTo from 'widget/components/FooterReplyTo.vue'; import ChatInputWrap from 'widget/components/ChatInputWrap.vue'; import { BUS_EVENTS } from 'shared/constants/busEvents'; +import { CONVERSATION_STATUS } from 'shared/constants/messages'; import { sendEmailTranscript } from 'widget/api/conversation'; import { useRouter } from 'vue-router'; import { IFrameHelper } from '../helpers/utils'; import { CHATWOOT_ON_START_CONVERSATION } from '../constants/sdkEvents'; import { emitter } from 'shared/helpers/mitt'; +const CAPTAIN_HANDOFF_REPLY_THRESHOLD = 5; + export default { components: { ChatInputWrap, @@ -24,6 +27,7 @@ export default { data() { return { inReplyTo: null, + isRequestingHandoff: false, }; }, computed: { @@ -31,6 +35,7 @@ export default { conversationAttributes: 'conversationAttributes/getConversationParams', widgetColor: 'appConfig/getWidgetColor', conversationSize: 'conversation/getConversationSize', + captainReplyCount: 'conversation/getCaptainReplyCount', currentUser: 'contacts/getCurrentUser', isWidgetStyleFlat: 'appConfig/isWidgetStyleFlat', }), @@ -45,6 +50,12 @@ export default { showEmailTranscriptButton() { return this.hasEmail; }, + showCaptainHandoffButton() { + return ( + this.conversationAttributes.status === CONVERSATION_STATUS.PENDING && + this.captainReplyCount >= CAPTAIN_HANDOFF_REPLY_THRESHOLD + ); + }, hasEmail() { return this.currentUser && this.currentUser.has_email; }, @@ -58,7 +69,11 @@ export default { emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.toggleReplyTo); }, methods: { - ...mapActions('conversation', ['sendMessage', 'sendAttachment']), + ...mapActions('conversation', [ + 'sendMessage', + 'sendAttachment', + 'requestHandoff', + ]), ...mapActions('conversationAttributes', ['getAttributes']), async handleSendMessage(content) { await this.sendMessage({ @@ -90,6 +105,18 @@ export default { toggleReplyTo(message) { this.inReplyTo = message; }, + async handleRequestHandoff() { + try { + this.isRequestingHandoff = true; + await this.requestHandoff(); + } catch (error) { + emitter.emit(BUS_EVENTS.SHOW_ALERT, { + message: this.$t('CAPTAIN_HANDOFF.ERROR'), + }); + } finally { + this.isRequestingHandoff = false; + } + }, async sendTranscript() { if (this.hasEmail) { try { @@ -124,6 +151,17 @@ export default { :in-reply-to="inReplyTo" @dismiss="inReplyTo = null" /> + + {{ $t('CAPTAIN_HANDOFF.BUTTON_TEXT') }} + { + await requestHandoff(); + dispatch('conversationAttributes/getAttributes', {}, { root: true }); + }, + setCustomAttributes: async ( { commit, rootGetters }, customAttributes = {} diff --git a/app/javascript/widget/store/modules/conversation/getters.js b/app/javascript/widget/store/modules/conversation/getters.js index ef1cd1cc3..a74ac45e1 100644 --- a/app/javascript/widget/store/modules/conversation/getters.js +++ b/app/javascript/widget/store/modules/conversation/getters.js @@ -39,6 +39,15 @@ export const getters = { getMessageCount: _state => { return Object.values(_state.conversations).length; }, + getCaptainReplyCount: _state => { + return Object.values(_state.conversations).filter(message => { + const { message_type: messageType, sender = {} } = message; + return ( + messageType === MESSAGE_TYPE.OUTGOING && + sender.type === 'captain_assistant' + ); + }).length; + }, getUnreadMessageCount: _state => { const { userLastSeenAt } = _state.meta; return Object.values(_state.conversations).filter(chat => { diff --git a/config/routes.rb b/config/routes.rb index eff17e714..6c74d0b2d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -419,6 +419,7 @@ Rails.application.routes.draw do post :update_last_seen post :toggle_typing post :transcript + post :request_handoff get :toggle_status end end