diff --git a/app/controllers/api/v1/accounts/contacts/calls_controller.rb b/app/controllers/api/v1/accounts/contacts/calls_controller.rb new file mode 100644 index 000000000..4fd2d358d --- /dev/null +++ b/app/controllers/api/v1/accounts/contacts/calls_controller.rb @@ -0,0 +1,67 @@ +class Api::V1::Accounts::Contacts::CallsController < Api::V1::Accounts::BaseController + before_action :fetch_contact + + def create + # Find a Voice channel + voice_inbox = Current.account.inboxes.find_by(channel_type: 'Channel::Voice') + + if voice_inbox.blank? + render json: { error: 'No Voice channel found' }, status: :unprocessable_entity + return + end + + if @contact.phone_number.blank? + render json: { error: 'Contact has no phone number' }, status: :unprocessable_entity + return + end + + begin + # Initiate the call using the channel's implementation + voice_inbox.channel.initiate_call(to: @contact.phone_number) + + # Create a new conversation for this call if needed + conversation = find_or_create_conversation(voice_inbox) + + render json: conversation + rescue StandardError => e + Rails.logger.error("Error initiating call: #{e.message}") + render json: { error: e.message }, status: :unprocessable_entity + end + end + + private + + def fetch_contact + @contact = Current.account.contacts.find(params[:contact_id]) + end + + def find_or_create_conversation(inbox) + conversation = inbox.conversations.where(contact_id: @contact.id).last + + if conversation.nil? || !conversation.open? + # Find or create a contact_inbox for this contact and inbox + contact_inbox = ContactInbox.find_or_create_by!( + contact_id: @contact.id, + inbox_id: inbox.id + ) + + conversation = ::Conversation.create!( + account_id: Current.account.id, + inbox_id: inbox.id, + contact_id: @contact.id, + contact_inbox_id: contact_inbox.id, + status: :open + ) + + # Add a note about the call being initiated + Messages::MessageBuilder.new( + user: Current.user, + conversation: conversation, + message_type: :activity, + content: "Voice call initiated to #{@contact.phone_number}" + ).perform + end + + conversation + end +end \ No newline at end of file diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 011faaf28..449493ef0 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -79,7 +79,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController end def create_channel - return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type]) + return unless %w[web_widget api email line telegram whatsapp sms voice].include?(permitted_params[:channel][:type]) account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type)) end @@ -145,7 +145,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController 'line' => Channel::Line, 'telegram' => Channel::Telegram, 'whatsapp' => Channel::Whatsapp, - 'sms' => Channel::Sms + 'sms' => Channel::Sms, + 'voice' => Channel::Voice }[permitted_params[:channel][:type]] end diff --git a/app/helpers/api/v1/inboxes_helper.rb b/app/helpers/api/v1/inboxes_helper.rb index d734a346e..6abfc5806 100644 --- a/app/helpers/api/v1/inboxes_helper.rb +++ b/app/helpers/api/v1/inboxes_helper.rb @@ -107,7 +107,8 @@ module Api::V1::InboxesHelper 'line' => Current.account.line_channels, 'telegram' => Current.account.telegram_channels, 'whatsapp' => Current.account.whatsapp_channels, - 'sms' => Current.account.sms_channels + 'sms' => Current.account.sms_channels, + 'voice' => Current.account.voice_channels }[permitted_params[:channel][:type]] end diff --git a/app/javascript/dashboard/api/channels/voice.js b/app/javascript/dashboard/api/channels/voice.js new file mode 100644 index 000000000..bae9d8f9b --- /dev/null +++ b/app/javascript/dashboard/api/channels/voice.js @@ -0,0 +1,21 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class VoiceAPI extends ApiClient { + constructor() { + // Use empty string for resource to avoid duplicate 'accounts' in URL + super('', { accountScoped: true }); + } + + // Initiate a call to a contact + initiateCall(contactId) { + // Get the account ID from the current URL + const accountId = this.accountIdFromRoute; + // Make sure we have the right endpoint path + return axios.post( + `/api/v1/accounts/${accountId}/contacts/${contactId}/call` + ); + } +} + +export default new VoiceAPI(); \ No newline at end of file diff --git a/app/javascript/dashboard/components/widgets/ChannelItem.vue b/app/javascript/dashboard/components/widgets/ChannelItem.vue index 4f9751c4e..d9dc62bf5 100644 --- a/app/javascript/dashboard/components/widgets/ChannelItem.vue +++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue @@ -40,10 +40,10 @@ export default { this.enabledFeatures.channel_instagram && this.hasInstagramConfigured ); } - return [ 'website', 'twilio', + 'voice', 'api', 'whatsapp', 'sms', diff --git a/app/javascript/dashboard/helper/inbox.js b/app/javascript/dashboard/helper/inbox.js index ff6d73c84..dbc6cc719 100644 --- a/app/javascript/dashboard/helper/inbox.js +++ b/app/javascript/dashboard/helper/inbox.js @@ -3,6 +3,7 @@ export const INBOX_TYPES = { FB: 'Channel::FacebookPage', TWITTER: 'Channel::TwitterProfile', TWILIO: 'Channel::TwilioSms', + VOICE: 'Channel::Voice', WHATSAPP: 'Channel::Whatsapp', API: 'Channel::Api', EMAIL: 'Channel::Email', @@ -22,6 +23,7 @@ const INBOX_ICON_MAP_FILL = { [INBOX_TYPES.TELEGRAM]: 'i-ri-telegram-fill', [INBOX_TYPES.LINE]: 'i-ri-line-fill', [INBOX_TYPES.INSTAGRAM]: 'i-ri-instagram-fill', + [INBOX_TYPES.VOICE]: 'i-ri-phone-fill', }; const DEFAULT_ICON_FILL = 'i-ri-chat-1-fill'; @@ -36,6 +38,7 @@ const INBOX_ICON_MAP_LINE = { [INBOX_TYPES.TELEGRAM]: 'i-ri-telegram-line', [INBOX_TYPES.LINE]: 'i-ri-line-line', [INBOX_TYPES.INSTAGRAM]: 'i-ri-instagram-line', + [INBOX_TYPES.VOICE]: 'i-ri-phone-line', }; const DEFAULT_ICON_LINE = 'i-ri-chat-1-line'; @@ -69,6 +72,9 @@ export const getReadableInboxByType = (type, phoneNumber) => { case INBOX_TYPES.TWILIO: return phoneNumber?.startsWith('whatsapp') ? 'whatsapp' : 'sms'; + + case INBOX_TYPES.VOICE: + return 'voice'; case INBOX_TYPES.WHATSAPP: return 'whatsapp'; @@ -105,6 +111,9 @@ export const getInboxClassByType = (type, phoneNumber) => { return phoneNumber?.startsWith('whatsapp') ? 'brand-whatsapp' : 'brand-sms'; + + case INBOX_TYPES.VOICE: + return 'phone'; case INBOX_TYPES.WHATSAPP: return 'brand-whatsapp'; diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index 9f78a9684..7b2d8d94d 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -379,6 +379,9 @@ "TWO": "{user} and {secondUser} are typing", "MULTIPLE": "{user} and {count} others are typing" }, + "VOICE_CALL": "Call", + "CALL_ERROR": "Failed to initiate call. Please try again.", + "CALL_INITIATED": "Call initiated successfully.", "COPILOT": { "TRY_THESE_PROMPTS": "Try these prompts" }, diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 5716e050c..c4627bde6 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -217,6 +217,37 @@ } } }, + "VOICE": { + "TITLE": "Voice Channel", + "DESC": "Integrate voice calling capabilities with your preferred provider.", + "PROVIDER": { + "LABEL": "Provider", + "PLACEHOLDER": "Select a voice provider" + }, + "PHONE_NUMBER": { + "LABEL": "Phone Number", + "PLACEHOLDER": "Please enter the phone number from which calls will be made.", + "REQUIRED": "This field is required", + "ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces." + }, + "TWILIO": { + "ACCOUNT_SID": { + "LABEL": "Account SID", + "PLACEHOLDER": "Please enter your Twilio Account SID", + "REQUIRED": "This field is required" + }, + "AUTH_TOKEN": { + "LABEL": "Auth Token", + "PLACEHOLDER": "Please enter your Twilio Auth Token", + "REQUIRED": "This field is required" + } + }, + "SUBMIT_BUTTON": "Create Voice Channel", + "API": { + "SUCCESS_MESSAGE": "Voice channel registered successfully", + "ERROR_MESSAGE": "Unable to create voice channel. Please try again." + } + }, "WHATSAPP": { "TITLE": "WhatsApp Channel", "DESC": "Start supporting your customers via WhatsApp.", @@ -758,6 +789,7 @@ "WEB_WIDGET": "Website", "TWITTER_PROFILE": "Twitter", "TWILIO_SMS": "Twilio SMS", + "VOICE": "Voice", "WHATSAPP": "WhatsApp", "SMS": "SMS", "EMAIL": "Email", diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue index b423ecfc6..cfcf197e1 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue @@ -4,6 +4,7 @@ import { useAlert } from 'dashboard/composables'; import { dynamicTime } from 'shared/helpers/timeHelper'; import { useAdmin } from 'dashboard/composables/useAdmin'; import ContactInfoRow from './ContactInfoRow.vue'; +import inboxMixin from 'shared/mixins/inboxMixin'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import SocialIcons from './SocialIcons.vue'; import EditContact from './EditContact.vue'; @@ -11,6 +12,7 @@ import ContactMergeModal from 'dashboard/modules/contact/ContactMergeModal.vue'; import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue'; import { BUS_EVENTS } from 'shared/constants/busEvents'; import NextButton from 'dashboard/components-next/button/Button.vue'; +import VoiceAPI from 'dashboard/api/channels/voice'; import { isAConversationRoute, @@ -29,6 +31,7 @@ export default { SocialIcons, ContactMergeModal, }, + mixins: [inboxMixin], props: { contact: { type: Object, @@ -51,6 +54,7 @@ export default { showEditModal: false, showMergeModal: false, showDeleteModal: false, + isCallLoading: false, }; }, computed: { @@ -138,6 +142,20 @@ export default { return ''; } }, + async initiateVoiceCall() { + if (!this.contact || !this.contact.id) return; + + this.isCallLoading = true; + try { + const response = await VoiceAPI.initiateCall(this.contact.id); + useAlert('Call initiated successfully', 'success'); + } catch (error) { + // Error handled with useAlert + useAlert('Failed to initiate call. Please try again.', 'error'); + } finally { + this.isCallLoading = false; + } + }, async deleteContact({ id }) { try { await this.$store.dispatch('contacts/delete', id); @@ -276,6 +294,16 @@ export default { /> + {{ $t('CONTACT_PANEL.NOT_AVAILABLE') }} - +
+ + +
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelFactory.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelFactory.vue index 7ea58e3e5..609365e20 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelFactory.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelFactory.vue @@ -10,6 +10,7 @@ import Whatsapp from './channels/Whatsapp.vue'; import Line from './channels/Line.vue'; import Telegram from './channels/Telegram.vue'; import Instagram from './channels/Instagram.vue'; +import Voice from './channels/Voice.vue'; const channelViewList = { facebook: Facebook, @@ -22,6 +23,7 @@ const channelViewList = { line: Line, telegram: Telegram, instagram: Instagram, + voice: Voice, }; export default defineComponent({ diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue index 8482dabc2..71f70cbf7 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue @@ -28,6 +28,7 @@ export default { { key: 'whatsapp', name: 'WhatsApp' }, { key: 'sms', name: 'SMS' }, { key: 'email', name: 'Email' }, + { key: 'voice', name: 'Voice' }, { key: 'api', name: apiChannelName || 'API', diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Voice.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Voice.vue new file mode 100644 index 000000000..1653bc638 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Voice.vue @@ -0,0 +1,195 @@ + + + \ No newline at end of file diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/ChannelName.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/ChannelName.vue index b7b0a2c1d..af964aadf 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/ChannelName.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/ChannelName.vue @@ -22,6 +22,7 @@ const i18nMap = { 'Channel::WebWidget': 'WEB_WIDGET', 'Channel::TwitterProfile': 'TWITTER_PROFILE', 'Channel::TwilioSms': 'TWILIO_SMS', + 'Channel::Voice': 'VOICE', 'Channel::Whatsapp': 'WHATSAPP', 'Channel::Sms': 'SMS', 'Channel::Email': 'EMAIL', diff --git a/app/javascript/dashboard/store/modules/inboxes.js b/app/javascript/dashboard/store/modules/inboxes.js index 32d91fb8e..ce0f156b4 100644 --- a/app/javascript/dashboard/store/modules/inboxes.js +++ b/app/javascript/dashboard/store/modules/inboxes.js @@ -207,6 +207,33 @@ export const actions = { throw error; } }, + createVoiceChannel: async ({ commit }, params) => { + try { + commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true }); + + // Create a formatted payload for the voice channel + const inboxParams = { + name: params.voice.name || `Voice (${params.voice.phone_number})`, + channel: { + type: 'Channel::Voice', + phone_number: params.voice.phone_number, + provider: params.voice.provider, + provider_config: params.voice.provider_config, + }, + }; + + // Use InboxesAPI to create the channel which handles authentication properly + const response = await InboxesAPI.create(inboxParams); + + commit(types.default.ADD_INBOXES, response.data); + commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false }); + sendAnalyticsEvent('voice'); + return response.data; + } catch (error) { + commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false }); + throw error; + } + }, createFBChannel: async ({ commit }, params) => { try { commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true }); diff --git a/app/javascript/shared/mixins/inboxMixin.js b/app/javascript/shared/mixins/inboxMixin.js index 273e9f8b4..30112c557 100644 --- a/app/javascript/shared/mixins/inboxMixin.js +++ b/app/javascript/shared/mixins/inboxMixin.js @@ -3,6 +3,7 @@ import { INBOX_TYPES } from 'dashboard/helper/inbox'; export const INBOX_FEATURES = { REPLY_TO: 'replyTo', REPLY_TO_OUTGOING: 'replyToOutgoing', + VOICE_CALL: 'voiceCall', }; // This is a single source of truth for inbox features @@ -15,6 +16,7 @@ export const INBOX_FEATURE_MAP = { INBOX_TYPES.WHATSAPP, INBOX_TYPES.TELEGRAM, INBOX_TYPES.API, + INBOX_TYPES.VOICE, ], [INBOX_FEATURES.REPLY_TO_OUTGOING]: [ INBOX_TYPES.WEB, @@ -22,22 +24,24 @@ export const INBOX_FEATURE_MAP = { INBOX_TYPES.WHATSAPP, INBOX_TYPES.TELEGRAM, INBOX_TYPES.API, + INBOX_TYPES.VOICE, ], + [INBOX_FEATURES.VOICE_CALL]: [INBOX_TYPES.VOICE], }; export default { computed: { channelType() { - return this.inbox.channel_type; + return this.inbox?.channel_type || ''; }, whatsAppAPIProvider() { - return this.inbox.provider || ''; + return this.inbox?.provider || ''; }, isAMicrosoftInbox() { - return this.isAnEmailChannel && this.inbox.provider === 'microsoft'; + return this.isAnEmailChannel && this.inbox?.provider === 'microsoft'; }, isAGoogleInbox() { - return this.isAnEmailChannel && this.inbox.provider === 'google'; + return this.isAnEmailChannel && this.inbox?.provider === 'google'; }, isAPIInbox() { return this.channelType === INBOX_TYPES.API; @@ -54,6 +58,9 @@ export default { isATwilioChannel() { return this.channelType === INBOX_TYPES.TWILIO; }, + isAVoiceChannel() { + return this.channelType === INBOX_TYPES.VOICE; + }, isALineChannel() { return this.channelType === INBOX_TYPES.LINE; }, diff --git a/app/jobs/webhooks/voice_transcription_job.rb b/app/jobs/webhooks/voice_transcription_job.rb new file mode 100644 index 000000000..f4e10a803 --- /dev/null +++ b/app/jobs/webhooks/voice_transcription_job.rb @@ -0,0 +1,28 @@ +module Webhooks + # This job handles voice transcriptions from Twilio when a voice channel is configured + class VoiceTranscriptionJob < ApplicationJob + queue_as :default + + def perform(params) + call_sid = params['CallSid'] + transcription_text = params['TranscriptionText'] + + return if call_sid.blank? || transcription_text.blank? + + # Find the conversation based on the call_sid stored in message metadata + message = Message.find_by('additional_attributes @> ?', { call_sid: call_sid }.to_json) + return if message.blank? + + conversation = message.conversation + + Message.create!( + account_id: conversation.account_id, + inbox_id: conversation.inbox_id, + conversation_id: conversation.id, + message_type: :incoming, + content: transcription_text, + sender: conversation.contact + ) + end + end +end \ No newline at end of file diff --git a/app/models/account.rb b/app/models/account.rb index decfc9d2e..afc8620c2 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -71,6 +71,7 @@ class Account < ApplicationRecord has_many :telegram_bots, dependent: :destroy_async has_many :telegram_channels, dependent: :destroy_async, class_name: '::Channel::Telegram' has_many :twilio_sms, dependent: :destroy_async, class_name: '::Channel::TwilioSms' + has_many :voice_channels, dependent: :destroy_async, class_name: '::Channel::Voice' has_many :twitter_profiles, dependent: :destroy_async, class_name: '::Channel::TwitterProfile' has_many :users, through: :account_users has_many :web_widgets, dependent: :destroy_async, class_name: '::Channel::WebWidget' diff --git a/app/models/channel/voice.rb b/app/models/channel/voice.rb new file mode 100644 index 000000000..50a0b3be3 --- /dev/null +++ b/app/models/channel/voice.rb @@ -0,0 +1,51 @@ +class Channel::Voice < ApplicationRecord + include Channelable + include Rails.application.routes.url_helpers + + self.table_name = 'channel_voice' + + validates :phone_number, presence: true, uniqueness: true + validates :provider, presence: true + + # Provider-specific configs stored in JSON + validates :provider_config, presence: true + + EDITABLE_ATTRS = [:phone_number, :provider, :provider_config].freeze + + def name + "#{provider.capitalize} Voice" + end + + def initiate_call(to:) + case provider + when 'twilio' + initiate_twilio_call(to) + # Add more providers as needed + # when 'other_provider' + # initiate_other_provider_call(to) + else + raise "Unsupported voice provider: #{provider}" + end + end + + private + + def initiate_twilio_call(to) + config = provider_config_hash + callback_url = Rails.application.routes.url_helpers.twiml_twilio_voice_url(host: ENV.fetch('FRONTEND_URL', 'http://localhost:3000')) + params = { from: phone_number, to: to, url: callback_url } + twilio_client(config).calls.create(**params) + end + + def twilio_client(config) + Twilio::REST::Client.new(config['account_sid'], config['auth_token']) + end + + def provider_config_hash + if provider_config.is_a?(Hash) + provider_config + else + JSON.parse(provider_config.to_s) + end + end +end \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index a4341927e..a919510c9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -31,6 +31,33 @@ en: hello: 'Hello world' + INBOX_MGMT: + ADD: + TWILIO_VOICE: + TITLE: 'Add Twilio Voice Channel' + DESC: 'Integrate Twilio Voice to accept and make voice calls through Chatwoot.' + PHONE_NUMBER: + LABEL: 'Phone Number' + PLACEHOLDER: '+1234567890' + ERROR: 'Please enter a valid phone number' + ACCOUNT_SID: + LABEL: 'Account SID' + PLACEHOLDER: 'Enter your Twilio Account SID' + REQUIRED: 'Account SID is required' + AUTH_TOKEN: + LABEL: 'Auth Token' + PLACEHOLDER: 'Enter your Twilio Auth Token' + REQUIRED: 'Auth Token is required' + SUBMIT_BUTTON: 'Create Channel' + API: + ERROR_MESSAGE: 'Failed to create Twilio Voice channel' + CONVERSATION: + VOICE_CALL: 'Voice Call' + CALL_ERROR: 'Failed to initiate voice call' + CALL_INITIATED: 'Voice call initiated successfully' + CONTACT_PANEL: + NEW_MESSAGE: 'New Message' + MERGE_CONTACT: 'Merge Contact' messages: reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions. reset_password_failure: Uh ho! We could not find any user with the specified email. diff --git a/config/routes.rb b/config/routes.rb index 46199db12..d349f46ac 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -104,6 +104,7 @@ Rails.application.routes.draw do post :retry end end + post :call, on: :member resources :assignments, only: [:create] resources :labels, only: [:create, :index] resource :participants, only: [:show, :create, :update, :destroy] @@ -152,6 +153,7 @@ Rails.application.routes.draw do resources :contact_inboxes, only: [:create] resources :labels, only: [:create, :index] resources :notes + post :call, to: 'calls#create' end end resources :csat_survey_responses, only: [:index] do @@ -477,6 +479,10 @@ Rails.application.routes.draw do namespace :twilio do resources :callback, only: [:create] resources :delivery_status, only: [:create] + resource :voice, only: [] do + get :twiml + post :transcription_callback + end end get 'microsoft/callback', to: 'microsoft/callbacks#show' diff --git a/db/migrate/20250426130000_create_channel_voice.rb b/db/migrate/20250426130000_create_channel_voice.rb new file mode 100644 index 000000000..18a3a8cf3 --- /dev/null +++ b/db/migrate/20250426130000_create_channel_voice.rb @@ -0,0 +1,15 @@ +class CreateChannelVoice < ActiveRecord::Migration[7.0] + def change + create_table :channel_voice do |t| + t.string :phone_number, null: false + t.string :provider, null: false, default: 'twilio' + t.jsonb :provider_config, null: false + t.integer :account_id, null: false + t.jsonb :additional_attributes, default: {} + + t.timestamps + + t.index :phone_number, unique: true + end + end +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 494dc4fcb..5f174ffed 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2025_04_16_182131) do +ActiveRecord::Schema[7.0].define(version: 2025_04_26_140000) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -442,6 +442,17 @@ ActiveRecord::Schema[7.0].define(version: 2025_04_16_182131) do t.index ["account_id", "profile_id"], name: "index_channel_twitter_profiles_on_account_id_and_profile_id", unique: true end + create_table "channel_voice", force: :cascade do |t| + t.string "phone_number", null: false + t.string "provider", default: "twilio", null: false + t.jsonb "provider_config", null: false + t.integer "account_id", null: false + t.jsonb "additional_attributes", default: {} + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["phone_number"], name: "index_channel_voice_on_phone_number", unique: true + end + create_table "channel_web_widgets", id: :serial, force: :cascade do |t| t.string "website_url" t.integer "account_id" diff --git a/public/assets/images/dashboard/channels/voice.png b/public/assets/images/dashboard/channels/voice.png new file mode 100644 index 000000000..7c9481faf Binary files /dev/null and b/public/assets/images/dashboard/channels/voice.png differ