diff --git a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue index 304e55347..c95eba8c8 100644 --- a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue +++ b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue @@ -8,6 +8,7 @@ import CardLayout from 'dashboard/components-next/CardLayout.vue'; import Button from 'dashboard/components-next/button/Button.vue'; import LiveChatCampaignDetails from './LiveChatCampaignDetails.vue'; import SMSCampaignDetails from './SMSCampaignDetails.vue'; +import VoiceCampaignDetails from './VoiceCampaignDetails.vue'; const props = defineProps({ title: { @@ -22,6 +23,10 @@ const props = defineProps({ type: Boolean, default: false, }, + isVoiceType: { + type: Boolean, + default: false, + }, isEnabled: { type: Boolean, default: false, @@ -67,6 +72,12 @@ const campaignStatus = computed(() => { ? t('CAMPAIGN.LIVE_CHAT.CARD.STATUS.ENABLED') : t('CAMPAIGN.LIVE_CHAT.CARD.STATUS.DISABLED'); } + + if (props.isVoiceType) { + return props.status === STATUS_COMPLETED + ? t('CAMPAIGN.VOICE.CARD.STATUS.COMPLETED') + : t('CAMPAIGN.VOICE.CARD.STATUS.SCHEDULED'); + } return props.status === STATUS_COMPLETED ? t('CAMPAIGN.SMS.CARD.STATUS.COMPLETED') @@ -108,6 +119,12 @@ const inboxIcon = computed(() => { :inbox-name="inboxName" :inbox-icon="inboxIcon" /> + { +import { computed } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { getInboxIconByType } from 'dashboard/helper/inbox'; + +import Avatar from 'dashboard/components-next/avatar/Avatar.vue'; +import Icon from 'dashboard/components-next/icon/Icon.vue'; + +const props = defineProps({ + sender: { + type: Object, + default: null, + }, + inboxName: { + type: String, + default: '', + }, + inboxIcon: { + type: String, + default: '', + }, + campaign: { + type: Object, + default: () => ({}), + }, +}); + +const { t } = useI18n(); + +const senderName = computed(() => + props.sender?.name || t('CAMPAIGN.VOICE.CARD.CAMPAIGN_DETAILS.BOT') +); + +const senderThumbnail = computed(() => props.sender?.thumbnail || ''); + + + + + + {{ t('CAMPAIGN.VOICE.CARD.CAMPAIGN_DETAILS.SENT_BY') }} + + + + + {{ senderName }} + + + + {{ t('CAMPAIGN.VOICE.CARD.CAMPAIGN_DETAILS.FROM') }} + + + + + {{ inboxName }} + + + + \ No newline at end of file diff --git a/app/javascript/dashboard/components-next/Campaigns/EmptyState/CampaignEmptyStateContent.js b/app/javascript/dashboard/components-next/Campaigns/EmptyState/CampaignEmptyStateContent.js index 4f409196f..c0b3edf3f 100644 --- a/app/javascript/dashboard/components-next/Campaigns/EmptyState/CampaignEmptyStateContent.js +++ b/app/javascript/dashboard/components-next/Campaigns/EmptyState/CampaignEmptyStateContent.js @@ -37,7 +37,7 @@ export const ONGOING_CAMPAIGN_EMPTY_STATE_CONTENT = [ id: 1, name: 'Jamie Lee', }, - message: 'Hello! 👋 Any questions on pricing? I’m here to help!', + message: 'Hello! 👋 Any questions on pricing? I am here to help!', campaign_status: 'active', enabled: false, campaign_type: 'ongoing', @@ -60,7 +60,8 @@ export const ONGOING_CAMPAIGN_EMPTY_STATE_CONTENT = [ }, sender: { id: 1, - name: 'Chatwoot', + name: 'Alexa Rivera', + thumbnail: 'AR', }, message: 'Hi! Chatwoot here. Need help setting up? Let me know!', campaign_status: 'active', @@ -88,7 +89,7 @@ export const ONGOING_CAMPAIGN_EMPTY_STATE_CONTENT = [ name: 'Chris Barlow', }, message: - 'Hi there! 👋 I’m here for any questions you may have. Let’s chat!', + 'Hi there! 👋 I am here for any questions you may have. Let us chat!', campaign_status: 'active', enabled: true, campaign_type: 'ongoing', @@ -166,7 +167,7 @@ export const ONE_OFF_CAMPAIGN_EMPTY_STATE_CONTENT = [ phone_number: '+29818373149903', provider: 'default', }, - message: 'Hello! We’re excited to have your business with us!', + message: 'Hello! We are excited to have your business with us!', campaign_status: 'active', enabled: true, campaign_type: 'one_off', @@ -210,3 +211,114 @@ export const ONE_OFF_CAMPAIGN_EMPTY_STATE_CONTENT = [ updated_at: '2024-10-30T16:15:03.157Z', }, ]; + +export const VOICE_CAMPAIGN_EMPTY_STATE_CONTENT = [ + { + id: 1, + title: 'Signup Confirmation Call', + inbox: { + id: 10, + name: 'PaperLayer Phone Support', + channel_type: 'Channel::Voice', + avatar_url: '', + phone_number: '+14155552671', + }, + message: 'Hello! 👋 Thanks for signing up with PaperLayer. I am calling to confirm your account setup and see if you have any questions about getting started.', + campaign_status: 'scheduled', + enabled: true, + campaign_type: 'voice', + scheduled_at: new Date('2024-11-16T20:43:08.000Z').getTime(), + audience: [ + { id: 4, type: 'Label', title: 'Support Customers' }, + { id: 5, type: 'Label', title: 'Active Users' }, + ], + sender: { + id: 1, + name: 'Chris Barlow', + thumbnail: 'CB' + }, + created_at: '2024-11-15T13:13:08.496Z', + updated_at: '2024-11-15T13:15:38.698Z', + }, + { + id: 3, + title: 'Support Ticket Follow-Up', + inbox: { + id: 10, + name: 'PaperLayer Phone Support', + channel_type: 'Channel::Voice', + avatar_url: '', + phone_number: '+14155552671', + }, + message: 'Hi, this is PaperLayer support calling to follow up on your recent ticket #12345. Has your issue been resolved to your satisfaction? If not, I can connect you with a specialist right away.', + campaign_status: 'completed', + enabled: true, + campaign_type: 'voice', + scheduled_at: new Date('2024-11-10T15:30:00.000Z').getTime(), + audience: [ + { id: 1, type: 'Label', title: 'Enterprise' }, + { id: 6, type: 'Label', title: 'Premium' }, + ], + sender: { + id: 2, + name: 'Sarah Wilson', + thumbnail: 'SW' + }, + created_at: '2024-11-10T13:14:00.168Z', + updated_at: '2024-11-10T13:15:38.707Z', + }, + { + id: 2, + title: 'Appointment Reminder', + inbox: { + id: 10, + name: 'PaperLayer Phone Support', + channel_type: 'Channel::Voice', + avatar_url: '', + phone_number: '+14155552671', + }, + message: 'Hello, this is a reminder about your upcoming consultation scheduled for tomorrow at 2:00 PM. Would you like to confirm this appointment or would you prefer to reschedule?', + campaign_status: 'scheduled', + enabled: true, + campaign_type: 'voice', + scheduled_at: new Date('2024-11-20T18:00:00.000Z').getTime(), + audience: [ + { id: 7, type: 'Label', title: 'Consultation Clients' }, + { id: 8, type: 'Label', title: 'New Customers' }, + ], + sender: { + id: 3, + name: 'Michael Thompson', + thumbnail: 'MT' + }, + created_at: '2024-11-12T09:30:45.123Z', + updated_at: '2024-11-12T09:30:45.123Z', + }, + { + id: 4, + title: 'Customer Feedback Survey', + inbox: { + id: 10, + name: 'PaperLayer Phone Support', + channel_type: 'Channel::Voice', + avatar_url: '', + phone_number: '+14155552671', + }, + message: 'Hello, this is PaperLayer reaching out for your valuable feedback. We noticed you\'ve been using our service for 30 days now. I\'d like to ask a few quick questions about your experience. Your feedback helps us improve our service. Would you have a moment to share your thoughts?', + campaign_status: 'scheduled', + enabled: true, + campaign_type: 'voice', + scheduled_at: new Date('2024-11-25T14:15:00.000Z').getTime(), + audience: [ + { id: 9, type: 'Label', title: 'Active 30+ Days' }, + { id: 10, type: 'Label', title: 'Product Users' }, + ], + sender: { + id: 4, + name: 'Jessica Rivera', + thumbnail: 'JR' + }, + created_at: '2024-11-18T10:45:23.789Z', + updated_at: '2024-11-18T10:45:23.789Z', + } +]; \ No newline at end of file diff --git a/app/javascript/dashboard/components-next/Campaigns/EmptyState/CustomEmptyStateLayout.vue b/app/javascript/dashboard/components-next/Campaigns/EmptyState/CustomEmptyStateLayout.vue new file mode 100644 index 000000000..87037b09e --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/EmptyState/CustomEmptyStateLayout.vue @@ -0,0 +1,55 @@ + + + + + + + + + + + + + {{ title }} + + + {{ subtitle }} + + + + + + + + + + \ No newline at end of file diff --git a/app/javascript/dashboard/components-next/Campaigns/EmptyState/VoiceCampaignEmptyState.vue b/app/javascript/dashboard/components-next/Campaigns/EmptyState/VoiceCampaignEmptyState.vue new file mode 100644 index 000000000..b50fdef5c --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/EmptyState/VoiceCampaignEmptyState.vue @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + {{ t('CAMPAIGN.VOICE.EMPTY_STATE.JS_API_DESCRIPTION') }} + + + chatwoot.triggerVoiceCampaign({ + campaignId: 'CAMPAIGN_ID', + user: { + name: 'John Doe', + phone: '+1234567890' + } + }); + + + + + \ No newline at end of file diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/CampaignList.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/CampaignList.vue index 183573606..8b18df4a7 100644 --- a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/CampaignList.vue +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/CampaignList.vue @@ -10,6 +10,10 @@ defineProps({ type: Boolean, default: false, }, + isVoiceType: { + type: Boolean, + default: false, + }, }); const emit = defineEmits(['edit', 'delete']); @@ -31,6 +35,7 @@ const handleDelete = campaign => emit('delete', campaign); :inbox="campaign.inbox" :scheduled-at="campaign.scheduled_at" :is-live-chat-type="isLiveChatType" + :is-voice-type="isVoiceType" @edit="handleEdit(campaign)" @delete="handleDelete(campaign)" /> diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/VoiceCampaign/VoiceCampaignDialog.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/VoiceCampaign/VoiceCampaignDialog.vue new file mode 100644 index 000000000..70f048f5c --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/VoiceCampaign/VoiceCampaignDialog.vue @@ -0,0 +1,60 @@ + + + + + + {{ t(`CAMPAIGN.VOICE.CREATE.TITLE`) }} + + + + \ No newline at end of file diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/VoiceCampaign/VoiceCampaignForm.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/VoiceCampaign/VoiceCampaignForm.vue new file mode 100644 index 000000000..f5bb570e4 --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/VoiceCampaign/VoiceCampaignForm.vue @@ -0,0 +1,189 @@ + + + + + + + + + + + {{ t('CAMPAIGN.VOICE.CREATE.FORM.INBOX.LABEL') }} + + + + + + + {{ t('CAMPAIGN.VOICE.CREATE.FORM.AUDIENCE.LABEL') }} + + + + + + + + + + + + \ No newline at end of file diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue index 90cb67c4f..2021f38dc 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue @@ -15,6 +15,7 @@ import WhatsAppOptions from './WhatsAppOptions.vue'; const props = defineProps({ attachedFiles: { type: Array, default: () => [] }, isWhatsappInbox: { type: Boolean, default: false }, + isVoiceInbox: { type: Boolean, default: false }, isEmailOrWebWidgetInbox: { type: Boolean, default: false }, isTwilioSmsInbox: { type: Boolean, default: false }, messageTemplates: { type: Array, default: () => [] }, @@ -113,6 +114,12 @@ const { onFileUpload } = useFileUpload({ }); const sendButtonLabel = computed(() => { + // For voice inboxes, show "Make a Call" instead of "Send" + if (props.isVoiceInbox) { + return t('COMPOSE_NEW_CONVERSATION.FORM.ACTION_BUTTONS.MAKE_CALL'); + } + + // For all other inboxes, show the standard "Send" label const keyCode = isEditorHotKeyEnabled('cmd_enter') ? '⌘ + ↵' : '↵'; return t('COMPOSE_NEW_CONVERSATION.FORM.ACTION_BUTTONS.SEND', { keyCode, @@ -157,7 +164,7 @@ useKeyboardEvents(keyboardEvents); @send-message="emit('sendWhatsappMessage', $event)" /> @@ -197,7 +204,7 @@ useKeyboardEvents(keyboardEvents); /> ({ isWhatsapp: props.targetInbox?.channelType === INBOX_TYPES.WHATSAPP, isWebWidget: props.targetInbox?.channelType === INBOX_TYPES.WEB, isApi: props.targetInbox?.channelType === INBOX_TYPES.API, + isVoice: props.targetInbox?.channelType === INBOX_TYPES.VOICE, isEmailOrWebWidget: props.targetInbox?.channelType === INBOX_TYPES.EMAIL || props.targetInbox?.channelType === INBOX_TYPES.WEB, @@ -87,7 +88,7 @@ const inboxChannelType = computed(() => props.targetInbox?.channelType || ''); const validationRules = computed(() => ({ selectedContact: { required }, targetInbox: { required }, - message: { required: requiredIf(!inboxTypes.value.isWhatsapp) }, + message: { required: requiredIf(!inboxTypes.value.isWhatsapp && !inboxTypes.value.isVoice) }, subject: { required: requiredIf(inboxTypes.value.isEmail) }, })); @@ -311,7 +312,7 @@ const handleSendWhatsappMessage = async ({ message, templateParams }) => { /> { /> @@ -329,6 +330,7 @@ const handleSendWhatsappMessage = async ({ message, templateParams }) => { { label: t('SIDEBAR.SMS'), to: accountScopedRoute('campaigns_sms_index'), }, + { + name: 'Voice', + label: t('SIDEBAR.VOICE'), + to: accountScopedRoute('campaigns_voice_index'), + }, ], }, { diff --git a/app/javascript/dashboard/components/widgets/FloatingCallWidget.vue b/app/javascript/dashboard/components/widgets/FloatingCallWidget.vue index 2bedaad16..1125b0394 100644 --- a/app/javascript/dashboard/components/widgets/FloatingCallWidget.vue +++ b/app/javascript/dashboard/components/widgets/FloatingCallWidget.vue @@ -3,6 +3,7 @@ import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'; import { useStore } from 'vuex'; import { useAlert } from 'dashboard/composables'; import { useI18n } from 'vue-i18n'; +import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper'; import VoiceAPI from 'dashboard/api/channels/voice'; import ContactAPI from 'dashboard/api/contacts'; import DashboardAudioNotificationHelper from 'dashboard/helper/AudioAlerts/DashboardAudioNotificationHelper'; @@ -322,7 +323,7 @@ export default { }, 300); }; - // Accept incoming call + // Accept incoming call and redirect to conversation const acceptCall = async () => { console.log('Accepting incoming call with SID:', incomingCall.value?.callSid); @@ -380,6 +381,19 @@ export default { // Emit event emit('callJoined'); + + // IMPORTANT: Redirect to the conversation view + if (conversationId) { + const accountId = this.$route.params.accountId; + const path = frontendURL( + conversationUrl({ + accountId, + id: conversationId, + }) + ); + console.log(`Redirecting to conversation path: ${path}`); + this.$router.push({ path }); + } } else { throw new Error('Failed to join call via WebRTC or phone'); } diff --git a/app/javascript/dashboard/i18n/locale/en/campaign.json b/app/javascript/dashboard/i18n/locale/en/campaign.json index e2418d52e..980185e87 100644 --- a/app/javascript/dashboard/i18n/locale/en/campaign.json +++ b/app/javascript/dashboard/i18n/locale/en/campaign.json @@ -1,5 +1,9 @@ { "CAMPAIGN": { + "BADGE": { + "ACTIVE": "Active", + "COMPLETED": "Completed" + }, "LIVE_CHAT": { "HEADER_TITLE": "Live chat campaigns", "NEW_CAMPAIGN": "Create campaign", @@ -137,6 +141,80 @@ } } }, + "VOICE": { + "HEADER_TITLE": "Voice campaigns", + "NEW_CAMPAIGN": "Create campaign", + "EMPTY_STATE": { + "TITLE": "No voice campaigns are available", + "SUBTITLE": "Make customer outreach more personal with AI-powered voice calls. Perfect for appointment reminders, verifications, and follow-ups that feel natural. Your AI assistant can adapt to customer responses, gather information, and enrich leads—saving your team valuable time.", + "JS_API_DESCRIPTION": "You can trigger voice campaigns programmatically using our JavaScript API:" + }, + "CARD": { + "STATUS": { + "COMPLETED": "Completed", + "SCHEDULED": "Scheduled" + }, + "CAMPAIGN_DETAILS": { + "SENT_BY": "Sent by ", + "FROM": " from ", + "ON": " on ", + "BOT": "Bot" + } + }, + "EMPTY_STATE_BADGE": { + "ENABLED": "Enabled", + "DISABLED": "Disabled" + }, + "DETAILS": { + "MESSAGE": "Message", + "INBOX": "Inbox", + "AUDIENCE": "Audience", + "SCHEDULED_AT": "Scheduled at" + }, + "CREATE": { + "TITLE": "Create voice campaign", + "FORM": { + "TITLE": { + "LABEL": "Title", + "PLACEHOLDER": "Enter the title of your voice campaign", + "ERROR": "Title is required" + }, + "MESSAGE": { + "LABEL": "Call Script", + "PLACEHOLDER": "Add the prompt to guide the AI during this call. Be specific about tone, information to gather, and how to handle different responses.", + "ERROR": "Call script is required" + }, + "INBOX": { + "LABEL": "Voice Inbox", + "PLACEHOLDER": "Select voice inbox", + "ERROR": "Voice inbox is required" + }, + "SENT_BY": { + "LABEL": "Sent by", + "PLACEHOLDER": "Select sender", + "ERROR": "Sender is required" + }, + "AUDIENCE": { + "LABEL": "Audience", + "PLACEHOLDER": "Select audience labels", + "ERROR": "Audience is required" + }, + "SCHEDULED_AT": { + "LABEL": "Scheduled time", + "PLACEHOLDER": "Select when to start the campaign", + "ERROR": "Scheduled time is required" + }, + "BUTTONS": { + "CREATE": "Create", + "CANCEL": "Cancel" + }, + "API": { + "SUCCESS_MESSAGE": "Voice campaign created successfully", + "ERROR_MESSAGE": "There was an error. Please try again." + } + } + } + }, "CONFIRM_DELETE": { "TITLE": "Are you sure to delete?", "DESCRIPTION": "The delete action is permanent and cannot be reversed.", @@ -147,4 +225,4 @@ } } } -} +} \ No newline at end of file diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index fc4509809..1c02b1876 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -603,7 +603,8 @@ }, "ACTION_BUTTONS": { "DISCARD": "Discard", - "SEND": "Send ({keyCode})" + "SEND": "Send ({keyCode})", + "MAKE_CALL": "Make a Call" } } } diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 10a21245b..5b1b6126b 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -312,6 +312,7 @@ "CSAT": "CSAT", "LIVE_CHAT": "Live Chat", "SMS": "SMS", + "VOICE": "Voice", "CAMPAIGNS": "Campaigns", "ONGOING": "Ongoing", "ONE_OFF": "One off", diff --git a/app/javascript/dashboard/routes/dashboard/campaigns/campaigns.routes.js b/app/javascript/dashboard/routes/dashboard/campaigns/campaigns.routes.js index e0c1f3a17..abb551958 100644 --- a/app/javascript/dashboard/routes/dashboard/campaigns/campaigns.routes.js +++ b/app/javascript/dashboard/routes/dashboard/campaigns/campaigns.routes.js @@ -3,6 +3,7 @@ import { frontendURL } from 'dashboard/helper/URLHelper.js'; import CampaignsPageRouteView from './pages/CampaignsPageRouteView.vue'; import LiveChatCampaignsPage from './pages/LiveChatCampaignsPage.vue'; import SMSCampaignsPage from './pages/SMSCampaignsPage.vue'; +import VoiceCampaignsPage from './pages/VoiceCampaignsPage.vue'; import { FEATURE_FLAGS } from 'dashboard/featureFlags'; const meta = { @@ -50,6 +51,12 @@ const campaignsRoutes = { meta, component: SMSCampaignsPage, }, + { + path: 'voice', + name: 'campaigns_voice_index', + meta, + component: VoiceCampaignsPage, + }, ], }, ], diff --git a/app/javascript/dashboard/routes/dashboard/campaigns/pages/VoiceCampaignsPage.vue b/app/javascript/dashboard/routes/dashboard/campaigns/pages/VoiceCampaignsPage.vue new file mode 100644 index 000000000..0a0c81605 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/campaigns/pages/VoiceCampaignsPage.vue @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue index c49c32d66..311309fc5 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue @@ -21,6 +21,7 @@ import { getConversationDashboardRoute, } from '../../../../helper/routeHelpers'; import { emitter } from 'shared/helpers/mitt'; +import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper'; export default { components: { @@ -213,6 +214,19 @@ export default { window.app.$data.showCallWidget = true; } + // IMPORTANT: Redirect to the conversation view + if (conversation.id) { + const accountId = this.$route.params.accountId; + const path = frontendURL( + conversationUrl({ + accountId, + id: conversation.id, + }) + ); + console.log(`Redirecting to conversation path: ${path}`); + this.$router.push({ path }); + } + // After a brief delay, force update UI setTimeout(() => { this.$forceUpdate(); diff --git a/app/javascript/dashboard/store/modules/inboxes.js b/app/javascript/dashboard/store/modules/inboxes.js index 1c2e87f5d..defaed811 100644 --- a/app/javascript/dashboard/store/modules/inboxes.js +++ b/app/javascript/dashboard/store/modules/inboxes.js @@ -117,6 +117,12 @@ export const getters = { (item.channel_type === INBOX_TYPES.TWILIO && item.medium === 'sms') ); }, + getVoiceInboxes($state) { + return $state.records.filter( + item => + item.channel_type === INBOX_TYPES.VOICE + ); + }, dialogFlowEnabledInboxes($state) { return $state.records.filter( item => item.channel_type !== INBOX_TYPES.EMAIL diff --git a/app/javascript/shared/constants/campaign.js b/app/javascript/shared/constants/campaign.js index 90cd7a202..b1537ae6a 100644 --- a/app/javascript/shared/constants/campaign.js +++ b/app/javascript/shared/constants/campaign.js @@ -1,4 +1,5 @@ export const CAMPAIGN_TYPES = { ONGOING: 'ongoing', ONE_OFF: 'one_off', + VOICE: 'voice', }; diff --git a/app/services/contacts/contactable_inboxes_service.rb b/app/services/contacts/contactable_inboxes_service.rb index 92d4160fe..f34c6a896 100644 --- a/app/services/contacts/contactable_inboxes_service.rb +++ b/app/services/contacts/contactable_inboxes_service.rb @@ -22,6 +22,8 @@ class Contacts::ContactableInboxesService api_contactable_inbox(inbox) when 'Channel::WebWidget' website_contactable_inbox(inbox) + when 'Channel::Voice' + voice_contactable_inbox(inbox) end end @@ -70,4 +72,10 @@ class Contacts::ContactableInboxesService { source_id: "whatsapp:#{@contact.phone_number}", inbox: inbox } end end + + def voice_contactable_inbox(inbox) + return if @contact.phone_number.blank? + + { source_id: @contact.phone_number, inbox: inbox } + end end
+ {{ subtitle }} +
+ {{ t('CAMPAIGN.VOICE.EMPTY_STATE.JS_API_DESCRIPTION') }} +
+ chatwoot.triggerVoiceCampaign({ + campaignId: 'CAMPAIGN_ID', + user: { + name: 'John Doe', + phone: '+1234567890' + } + }); +