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