mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 11:08:04 +00:00 
			
		
		
		
	Merge branch 'release/4.5.1'
This commit is contained in:
		| @@ -0,0 +1,20 @@ | |||||||
|  | class Api::V1::Accounts::AssignmentPolicies::InboxesController < Api::V1::Accounts::BaseController | ||||||
|  |   before_action :fetch_assignment_policy | ||||||
|  |   before_action -> { check_authorization(AssignmentPolicy) } | ||||||
|  |  | ||||||
|  |   def index | ||||||
|  |     @inboxes = @assignment_policy.inboxes | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def fetch_assignment_policy | ||||||
|  |     @assignment_policy = Current.account.assignment_policies.find( | ||||||
|  |       params[:assignment_policy_id] | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def permitted_params | ||||||
|  |     params.permit(:assignment_policy_id) | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -0,0 +1,36 @@ | |||||||
|  | class Api::V1::Accounts::AssignmentPoliciesController < Api::V1::Accounts::BaseController | ||||||
|  |   before_action :fetch_assignment_policy, only: [:show, :update, :destroy] | ||||||
|  |   before_action :check_authorization | ||||||
|  |  | ||||||
|  |   def index | ||||||
|  |     @assignment_policies = Current.account.assignment_policies | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def show; end | ||||||
|  |  | ||||||
|  |   def create | ||||||
|  |     @assignment_policy = Current.account.assignment_policies.create!(assignment_policy_params) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def update | ||||||
|  |     @assignment_policy.update!(assignment_policy_params) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def destroy | ||||||
|  |     @assignment_policy.destroy! | ||||||
|  |     head :ok | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def fetch_assignment_policy | ||||||
|  |     @assignment_policy = Current.account.assignment_policies.find(params[:id]) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def assignment_policy_params | ||||||
|  |     params.require(:assignment_policy).permit( | ||||||
|  |       :name, :description, :assignment_order, :conversation_priority, | ||||||
|  |       :fair_distribution_limit, :fair_distribution_window, :enabled | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -0,0 +1,46 @@ | |||||||
|  | class Api::V1::Accounts::Inboxes::AssignmentPoliciesController < Api::V1::Accounts::BaseController | ||||||
|  |   before_action :fetch_inbox | ||||||
|  |   before_action :fetch_assignment_policy, only: [:create] | ||||||
|  |   before_action -> { check_authorization(AssignmentPolicy) } | ||||||
|  |   before_action :validate_assignment_policy, only: [:show, :destroy] | ||||||
|  |  | ||||||
|  |   def show | ||||||
|  |     @assignment_policy = @inbox.assignment_policy | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def create | ||||||
|  |     # There should be only one assignment policy for an inbox. | ||||||
|  |     # If there is a new request to add an assignment policy, we will | ||||||
|  |     # delete the old one and attach the new policy | ||||||
|  |     remove_inbox_assignment_policy | ||||||
|  |     @inbox_assignment_policy = @inbox.create_inbox_assignment_policy!(assignment_policy: @assignment_policy) | ||||||
|  |     @assignment_policy = @inbox.assignment_policy | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def destroy | ||||||
|  |     remove_inbox_assignment_policy | ||||||
|  |     head :ok | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def remove_inbox_assignment_policy | ||||||
|  |     @inbox.inbox_assignment_policy&.destroy | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def fetch_inbox | ||||||
|  |     @inbox = Current.account.inboxes.find(permitted_params[:inbox_id]) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def fetch_assignment_policy | ||||||
|  |     @assignment_policy = Current.account.assignment_policies.find(permitted_params[:assignment_policy_id]) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def permitted_params | ||||||
|  |     params.permit(:assignment_policy_id, :inbox_id) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def validate_assignment_policy | ||||||
|  |     return render_not_found_error(I18n.t('errors.assignment_policy.not_found')) unless @inbox.assignment_policy | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -7,6 +7,7 @@ import { vOnClickOutside } from '@vueuse/components'; | |||||||
| import Button from 'dashboard/components-next/button/Button.vue'; | import Button from 'dashboard/components-next/button/Button.vue'; | ||||||
| import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue'; | import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue'; | ||||||
| import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue'; | import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue'; | ||||||
|  | import VoiceCallButton from 'dashboard/components-next/Contacts/VoiceCallButton.vue'; | ||||||
|  |  | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   selectedContact: { |   selectedContact: { | ||||||
| @@ -99,6 +100,11 @@ const closeMobileSidebar = () => { | |||||||
|                 :disabled="isUpdating" |                 :disabled="isUpdating" | ||||||
|                 @click="toggleBlock" |                 @click="toggleBlock" | ||||||
|               /> |               /> | ||||||
|  |               <VoiceCallButton | ||||||
|  |                 :phone="selectedContact?.phoneNumber" | ||||||
|  |                 :label="$t('CONTACT_PANEL.CALL')" | ||||||
|  |                 size="sm" | ||||||
|  |               /> | ||||||
|               <ComposeConversation :contact-id="contactId"> |               <ComposeConversation :contact-id="contactId"> | ||||||
|                 <template #trigger="{ toggle }"> |                 <template #trigger="{ toggle }"> | ||||||
|                   <Button |                   <Button | ||||||
|   | |||||||
| @@ -0,0 +1,91 @@ | |||||||
|  | <script setup> | ||||||
|  | import { computed, ref, useAttrs } from 'vue'; | ||||||
|  | import { useI18n } from 'vue-i18n'; | ||||||
|  | import { useMapGetter } from 'dashboard/composables/store'; | ||||||
|  | import { INBOX_TYPES } from 'dashboard/helper/inbox'; | ||||||
|  | import { useAlert } from 'dashboard/composables'; | ||||||
|  |  | ||||||
|  | import Button from 'dashboard/components-next/button/Button.vue'; | ||||||
|  | import Dialog from 'dashboard/components-next/dialog/Dialog.vue'; | ||||||
|  |  | ||||||
|  | const props = defineProps({ | ||||||
|  |   phone: { type: String, default: '' }, | ||||||
|  |   label: { type: String, default: '' }, | ||||||
|  |   icon: { type: [String, Object, Function], default: '' }, | ||||||
|  |   size: { type: String, default: 'sm' }, | ||||||
|  |   tooltipLabel: { type: String, default: '' }, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | defineOptions({ inheritAttrs: false }); | ||||||
|  | const attrs = useAttrs(); | ||||||
|  |  | ||||||
|  | const { t } = useI18n(); | ||||||
|  |  | ||||||
|  | const inboxesList = useMapGetter('inboxes/getInboxes'); | ||||||
|  | const voiceInboxes = computed(() => | ||||||
|  |   (inboxesList.value || []).filter( | ||||||
|  |     inbox => inbox.channel_type === INBOX_TYPES.VOICE | ||||||
|  |   ) | ||||||
|  | ); | ||||||
|  | const hasVoiceInboxes = computed(() => voiceInboxes.value.length > 0); | ||||||
|  |  | ||||||
|  | // Unified behavior: hide when no phone | ||||||
|  | const shouldRender = computed(() => hasVoiceInboxes.value && !!props.phone); | ||||||
|  |  | ||||||
|  | const dialogRef = ref(null); | ||||||
|  |  | ||||||
|  | const onClick = () => { | ||||||
|  |   if (voiceInboxes.value.length > 1) { | ||||||
|  |     dialogRef.value?.open(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   useAlert(t('CONTACT_PANEL.CALL_UNDER_DEVELOPMENT')); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const onPickInbox = () => { | ||||||
|  |   // Placeholder until actual call wiring happens | ||||||
|  |   useAlert(t('CONTACT_PANEL.CALL_UNDER_DEVELOPMENT')); | ||||||
|  |   dialogRef.value?.close(); | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <span class="contents"> | ||||||
|  |     <Button | ||||||
|  |       v-if="shouldRender" | ||||||
|  |       v-tooltip.top-end="tooltipLabel || null" | ||||||
|  |       v-bind="attrs" | ||||||
|  |       :label="label" | ||||||
|  |       :icon="icon" | ||||||
|  |       :size="size" | ||||||
|  |       @click="onClick" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <Dialog | ||||||
|  |       v-if="shouldRender && voiceInboxes.length > 1" | ||||||
|  |       ref="dialogRef" | ||||||
|  |       :title="$t('CONTACT_PANEL.VOICE_INBOX_PICKER.TITLE')" | ||||||
|  |       show-cancel-button | ||||||
|  |       :show-confirm-button="false" | ||||||
|  |       width="md" | ||||||
|  |     > | ||||||
|  |       <div class="flex flex-col gap-2"> | ||||||
|  |         <button | ||||||
|  |           v-for="inbox in voiceInboxes" | ||||||
|  |           :key="inbox.id" | ||||||
|  |           type="button" | ||||||
|  |           class="flex items-center justify-between w-full px-4 py-2 text-left rounded-lg hover:bg-n-alpha-2" | ||||||
|  |           @click="onPickInbox(inbox)" | ||||||
|  |         > | ||||||
|  |           <div class="flex items-center gap-2"> | ||||||
|  |             <span class="i-ri-phone-fill text-n-slate-10" /> | ||||||
|  |             <span class="text-sm text-n-slate-12">{{ inbox.name }}</span> | ||||||
|  |           </div> | ||||||
|  |           <span v-if="inbox.phone_number" class="text-xs text-n-slate-10"> | ||||||
|  |             {{ inbox.phone_number }} | ||||||
|  |           </span> | ||||||
|  |         </button> | ||||||
|  |       </div> | ||||||
|  |     </Dialog> | ||||||
|  |   </span> | ||||||
|  | </template> | ||||||
| @@ -17,6 +17,11 @@ | |||||||
|     "IP_ADDRESS": "IP Address", |     "IP_ADDRESS": "IP Address", | ||||||
|     "CREATED_AT_LABEL": "Created", |     "CREATED_AT_LABEL": "Created", | ||||||
|     "NEW_MESSAGE": "New message", |     "NEW_MESSAGE": "New message", | ||||||
|  |     "CALL": "Call", | ||||||
|  |     "CALL_UNDER_DEVELOPMENT": "Calling is under development", | ||||||
|  |     "VOICE_INBOX_PICKER": { | ||||||
|  |       "TITLE": "Choose a voice inbox" | ||||||
|  |     }, | ||||||
|     "CONVERSATIONS": { |     "CONVERSATIONS": { | ||||||
|       "NO_RECORDS_FOUND": "There are no previous conversations associated to this contact.", |       "NO_RECORDS_FOUND": "There are no previous conversations associated to this contact.", | ||||||
|       "TITLE": "Previous Conversations" |       "TITLE": "Previous Conversations" | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ | |||||||
|       "TEXT": "Texto", |       "TEXT": "Texto", | ||||||
|       "NUMBER": "Número", |       "NUMBER": "Número", | ||||||
|       "LINK": "Link", |       "LINK": "Link", | ||||||
|       "DATE": "Date", |       "DATE": "Data", | ||||||
|       "LIST": "Lista", |       "LIST": "Lista", | ||||||
|       "CHECKBOX": "Checkbox" |       "CHECKBOX": "Checkbox" | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -131,7 +131,7 @@ | |||||||
|       "CONVERSATION_CREATED": "Conversa Criada", |       "CONVERSATION_CREATED": "Conversa Criada", | ||||||
|       "CONVERSATION_UPDATED": "Conversa Atualizada", |       "CONVERSATION_UPDATED": "Conversa Atualizada", | ||||||
|       "MESSAGE_CREATED": "Mensagem Criada", |       "MESSAGE_CREATED": "Mensagem Criada", | ||||||
|       "CONVERSATION_RESOLVED": "Conversation Resolved", |       "CONVERSATION_RESOLVED": "Conversa resolvida", | ||||||
|       "CONVERSATION_OPENED": "Conversa Aberta" |       "CONVERSATION_OPENED": "Conversa Aberta" | ||||||
|     }, |     }, | ||||||
|     "ACTIONS": { |     "ACTIONS": { | ||||||
| @@ -153,8 +153,8 @@ | |||||||
|       "OPEN_CONVERSATION": "Abrir conversa" |       "OPEN_CONVERSATION": "Abrir conversa" | ||||||
|     }, |     }, | ||||||
|     "MESSAGE_TYPES": { |     "MESSAGE_TYPES": { | ||||||
|       "INCOMING": "Incoming Message", |       "INCOMING": "Mensagem Recebida", | ||||||
|       "OUTGOING": "Outgoing Message" |       "OUTGOING": "Mensagem de Saída" | ||||||
|     }, |     }, | ||||||
|     "PRIORITY_TYPES": { |     "PRIORITY_TYPES": { | ||||||
|       "NONE": "Nenhuma", |       "NONE": "Nenhuma", | ||||||
|   | |||||||
| @@ -138,11 +138,11 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "WHATSAPP": { |     "WHATSAPP": { | ||||||
|       "HEADER_TITLE": "WhatsApp campaigns", |       "HEADER_TITLE": "Campanhas do WhatsApp", | ||||||
|       "NEW_CAMPAIGN": "Criar campanha", |       "NEW_CAMPAIGN": "Criar campanha", | ||||||
|       "EMPTY_STATE": { |       "EMPTY_STATE": { | ||||||
|         "TITLE": "No WhatsApp campaigns are available", |         "TITLE": "Nenhuma campanha do WhatsApp está disponível", | ||||||
|         "SUBTITLE": "Launch a WhatsApp campaign to reach your customers directly. Send offers or make announcements with ease. Click 'Create campaign' to get started." |         "SUBTITLE": "Inicie uma campanha do WhatsApp para atingir seus clientes diretamente. Envie ofertas ou faça anúncios facilmente. Clique em \"Criar campanha\" para começar." | ||||||
|       }, |       }, | ||||||
|       "CARD": { |       "CARD": { | ||||||
|         "STATUS": { |         "STATUS": { | ||||||
| @@ -155,7 +155,7 @@ | |||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "CREATE": { |       "CREATE": { | ||||||
|         "TITLE": "Create WhatsApp campaign", |         "TITLE": "Criar campanha do WhatsApp", | ||||||
|         "CANCEL_BUTTON_TEXT": "Cancelar", |         "CANCEL_BUTTON_TEXT": "Cancelar", | ||||||
|         "CREATE_BUTTON_TEXT": "Criar", |         "CREATE_BUTTON_TEXT": "Criar", | ||||||
|         "FORM": { |         "FORM": { | ||||||
| @@ -170,15 +170,15 @@ | |||||||
|             "ERROR": "Caixa de entrada obrigatória" |             "ERROR": "Caixa de entrada obrigatória" | ||||||
|           }, |           }, | ||||||
|           "TEMPLATE": { |           "TEMPLATE": { | ||||||
|             "LABEL": "WhatsApp Template", |             "LABEL": "Modelo do WhatsApp", | ||||||
|             "PLACEHOLDER": "Select a template", |             "PLACEHOLDER": "Selecione um modelo", | ||||||
|             "INFO": "Select a template to use for this campaign.", |             "INFO": "Selecione um modelo para usar para esta campanha.", | ||||||
|             "ERROR": "Template is required", |             "ERROR": "Modelo é obrigatório", | ||||||
|             "PREVIEW_TITLE": "Processar {templateName}", |             "PREVIEW_TITLE": "Processar {templateName}", | ||||||
|             "LANGUAGE": "Idioma", |             "LANGUAGE": "Idioma", | ||||||
|             "CATEGORY": "Categoria", |             "CATEGORY": "Categoria", | ||||||
|             "VARIABLES_LABEL": "Variáveis", |             "VARIABLES_LABEL": "Variáveis", | ||||||
|             "VARIABLE_PLACEHOLDER": "Enter value for {variable}" |             "VARIABLE_PLACEHOLDER": "Digite um valor para {variable}" | ||||||
|           }, |           }, | ||||||
|           "AUDIENCE": { |           "AUDIENCE": { | ||||||
|             "LABEL": "Público", |             "LABEL": "Público", | ||||||
| @@ -195,7 +195,7 @@ | |||||||
|             "CANCEL": "Cancelar" |             "CANCEL": "Cancelar" | ||||||
|           }, |           }, | ||||||
|           "API": { |           "API": { | ||||||
|             "SUCCESS_MESSAGE": "WhatsApp campaign created successfully", |             "SUCCESS_MESSAGE": "Campanha do WhatsApp criada com sucesso", | ||||||
|             "ERROR_MESSAGE": "Houve um erro. Por favor, tente novamente." |             "ERROR_MESSAGE": "Houve um erro. Por favor, tente novamente." | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -51,6 +51,6 @@ | |||||||
|     "PLACEHOLDER": "Insira a duração" |     "PLACEHOLDER": "Insira a duração" | ||||||
|   }, |   }, | ||||||
|   "CHANNEL_SELECTOR": { |   "CHANNEL_SELECTOR": { | ||||||
|     "COMING_SOON": "Coming Soon!" |     "COMING_SOON": "Em breve!" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -144,9 +144,9 @@ | |||||||
|       "AGENTS_LOADING": "Carregando agentes...", |       "AGENTS_LOADING": "Carregando agentes...", | ||||||
|       "ASSIGN_TEAM": "Atribuir time", |       "ASSIGN_TEAM": "Atribuir time", | ||||||
|       "DELETE": "Excluir conversa", |       "DELETE": "Excluir conversa", | ||||||
|       "OPEN_IN_NEW_TAB": "Open in new tab", |       "OPEN_IN_NEW_TAB": "Abrir em nova aba", | ||||||
|       "COPY_LINK": "Copy conversation link", |       "COPY_LINK": "Copiar link da conversa", | ||||||
|       "COPY_LINK_SUCCESS": "Conversation link copied to clipboard", |       "COPY_LINK_SUCCESS": "Link da conversa copiado", | ||||||
|       "API": { |       "API": { | ||||||
|         "AGENT_ASSIGNMENT": { |         "AGENT_ASSIGNMENT": { | ||||||
|           "SUCCESFUL": "ID da conversa {conversationId} atribuído para \"{agentName}\"", |           "SUCCESFUL": "ID da conversa {conversationId} atribuído para \"{agentName}\"", | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
|     "LIMIT_MESSAGES": { |     "LIMIT_MESSAGES": { | ||||||
|       "CONVERSATION": "Você excedeu o limite de conversas. O plano Hacker permite apenas 500 conversas.", |       "CONVERSATION": "Você excedeu o limite de conversas. O plano Hacker permite apenas 500 conversas.", | ||||||
|       "INBOXES": "Você excedeu o limite da caixa de entrada. O plano Hacker só suporta chat ao vivo do site. Caixas adicionais como e-mail, WhatsApp etc. requerem um plano pago.", |       "INBOXES": "Você excedeu o limite da caixa de entrada. O plano Hacker só suporta chat ao vivo do site. Caixas adicionais como e-mail, WhatsApp etc. requerem um plano pago.", | ||||||
|       "AGENTS": "You have exceeded the agent limit. Your plan only allows {allowedAgents} agents.", |       "AGENTS": "Você excedeu o limite do agente. Seu plano permite apenas {allowedAgents} agentes.", | ||||||
|       "NON_ADMIN": "Entre em contato com o administrador para atualizar o plano e continuar usando todos os recursos." |       "NON_ADMIN": "Entre em contato com o administrador para atualizar o plano e continuar usando todos os recursos." | ||||||
|     }, |     }, | ||||||
|     "TITLE": "Conta", |     "TITLE": "Conta", | ||||||
| @@ -134,7 +134,7 @@ | |||||||
|     "MULTISELECT": { |     "MULTISELECT": { | ||||||
|       "ENTER_TO_SELECT": "Digite enter para selecionar", |       "ENTER_TO_SELECT": "Digite enter para selecionar", | ||||||
|       "ENTER_TO_REMOVE": "Digite enter para remover", |       "ENTER_TO_REMOVE": "Digite enter para remover", | ||||||
|       "NO_OPTIONS": "List is empty", |       "NO_OPTIONS": "Lista vazia", | ||||||
|       "SELECT_ONE": "Selecione um", |       "SELECT_ONE": "Selecione um", | ||||||
|       "SELECT": "Selecionar" |       "SELECT": "Selecionar" | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -160,8 +160,8 @@ | |||||||
|         }, |         }, | ||||||
|         "SEND_CNAME_INSTRUCTIONS": { |         "SEND_CNAME_INSTRUCTIONS": { | ||||||
|           "API": { |           "API": { | ||||||
|             "SUCCESS_MESSAGE": "CNAME instructions sent successfully", |             "SUCCESS_MESSAGE": "Instruções do CNAME enviadas com sucesso", | ||||||
|             "ERROR_MESSAGE": "Error while sending CNAME instructions" |             "ERROR_MESSAGE": "Erro ao enviar as instruções CNAME" | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
| @@ -732,7 +732,7 @@ | |||||||
|         "HOME_PAGE_LINK": { |         "HOME_PAGE_LINK": { | ||||||
|           "LABEL": "Link da Página Inicial", |           "LABEL": "Link da Página Inicial", | ||||||
|           "PLACEHOLDER": "Link da página inicial do portal", |           "PLACEHOLDER": "Link da página inicial do portal", | ||||||
|           "ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'." |           "ERROR": "Digite uma URL válida. O link da página inicial deve começar com 'http://' ou 'https://'." | ||||||
|         }, |         }, | ||||||
|         "SLUG": { |         "SLUG": { | ||||||
|           "LABEL": "Slug", |           "LABEL": "Slug", | ||||||
| @@ -753,14 +753,14 @@ | |||||||
|           "HEADER": "Domínio personalizado", |           "HEADER": "Domínio personalizado", | ||||||
|           "LABEL": "Domínio personalizado:", |           "LABEL": "Domínio personalizado:", | ||||||
|           "DESCRIPTION": "Você pode hospedar seu portal em um domínio personalizado. Por exemplo, se seu site for meudominio.com e você quer o seu portal disponível em docs.meudominio.com, basta digitar isso neste campo.", |           "DESCRIPTION": "Você pode hospedar seu portal em um domínio personalizado. Por exemplo, se seu site for meudominio.com e você quer o seu portal disponível em docs.meudominio.com, basta digitar isso neste campo.", | ||||||
|           "STATUS_DESCRIPTION": "Your custom portal will start working as soon as it is verified.", |           "STATUS_DESCRIPTION": "Seu portal personalizado começará a funcionar assim que for verificado.", | ||||||
|           "PLACEHOLDER": "Domínio personalizado do portal", |           "PLACEHOLDER": "Domínio personalizado do portal", | ||||||
|           "EDIT_BUTTON": "Alterar", |           "EDIT_BUTTON": "Alterar", | ||||||
|           "ADD_BUTTON": "Adicionar domínio personalizado", |           "ADD_BUTTON": "Adicionar domínio personalizado", | ||||||
|           "STATUS": { |           "STATUS": { | ||||||
|             "LIVE": "Em tempo real", |             "LIVE": "Em tempo real", | ||||||
|             "PENDING": "Awaiting verification", |             "PENDING": "Aguardando verificação", | ||||||
|             "ERROR": "Verification failed" |             "ERROR": "Verificação falhou" | ||||||
|           }, |           }, | ||||||
|           "DIALOG": { |           "DIALOG": { | ||||||
|             "ADD_HEADER": "Adicionar domínio personalizado", |             "ADD_HEADER": "Adicionar domínio personalizado", | ||||||
| @@ -770,17 +770,17 @@ | |||||||
|             "LABEL": "Domínio personalizado", |             "LABEL": "Domínio personalizado", | ||||||
|             "PLACEHOLDER": "Domínio personalizado do portal", |             "PLACEHOLDER": "Domínio personalizado do portal", | ||||||
|             "ERROR": "Domínio personalizado é obrigatório", |             "ERROR": "Domínio personalizado é obrigatório", | ||||||
|             "FORMAT_ERROR": "Please enter a valid domain URL e.g. docs.yourdomain.com" |             "FORMAT_ERROR": "Por favor, insira um domínio de URL válido, ex.: docs.seudominio.com" | ||||||
|           }, |           }, | ||||||
|           "DNS_CONFIGURATION_DIALOG": { |           "DNS_CONFIGURATION_DIALOG": { | ||||||
|             "HEADER": "Configuração de DNS", |             "HEADER": "Configuração de DNS", | ||||||
|             "DESCRIPTION": "Faça o login na conta que você tem com seu provedor DNS e adicione um registro CNAME para subdomínio apontando para chatwoot.help", |             "DESCRIPTION": "Faça o login na conta que você tem com seu provedor DNS e adicione um registro CNAME para subdomínio apontando para chatwoot.help", | ||||||
|             "COPY": "Successfully copied CNAME", |             "COPY": "CNAME copiado com sucesso", | ||||||
|             "SEND_INSTRUCTIONS": { |             "SEND_INSTRUCTIONS": { | ||||||
|               "HEADER": "Send instructions", |               "HEADER": "Enviar instruções", | ||||||
|               "DESCRIPTION": "If you would prefer to have someone from your development team to handle this step, you can enter email address below, and we will send them the required instructions.", |               "DESCRIPTION": "Se você preferir ter alguém da sua equipe de desenvolvimento para lidar com essa etapa, você pode digitar o endereço de e-mail abaixo e nós enviaremos as instruções necessárias.", | ||||||
|               "PLACEHOLDER": "Enter their email", |               "PLACEHOLDER": "Insira o e-mail dele", | ||||||
|               "ERROR": "Enter a valid email address", |               "ERROR": "Insira um endereço de e-mail válido", | ||||||
|               "SEND_BUTTON": "Enviar" |               "SEND_BUTTON": "Enviar" | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|   | |||||||
| @@ -74,21 +74,21 @@ | |||||||
|       "DELETE_ALL_READ": "Todas as notificações lidas foram excluídas" |       "DELETE_ALL_READ": "Todas as notificações lidas foram excluídas" | ||||||
|     }, |     }, | ||||||
|     "REAUTHORIZE": { |     "REAUTHORIZE": { | ||||||
|       "TITLE": "Reauthorization Required", |       "TITLE": "Reautenticação necessária", | ||||||
|       "DESCRIPTION": "Your WhatsApp connection has expired. Please reconnect to continue receiving and sending messages.", |       "DESCRIPTION": "Sua conexão com o WhatsApp expirou. Por favor, reconecte para continuar recebendo e enviando mensagens.", | ||||||
|       "BUTTON_TEXT": "Reconnect WhatsApp", |       "BUTTON_TEXT": "Reconectar WhatsApp", | ||||||
|       "LOADING_FACEBOOK": "Loading Facebook SDK...", |       "LOADING_FACEBOOK": "Carregando SDK do Facebook...", | ||||||
|       "SUCCESS": "WhatsApp reconnected successfully", |       "SUCCESS": "WhatsApp reconectado com sucesso", | ||||||
|       "ERROR": "Failed to reconnect WhatsApp. Please try again.", |       "ERROR": "Falha ao reconectar o WhatsApp. Por favor, tente novamente.", | ||||||
|       "WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.", |       "WHATSAPP_APP_ID_MISSING": "WhatsApp App ID não está configurado. Por favor, contate seu administrador.", | ||||||
|       "WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.", |       "WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID não está configurado. Por favor, contate seu administrador.", | ||||||
|       "CONFIGURATION_ERROR": "Configuration error occurred during reauthorization.", |       "CONFIGURATION_ERROR": "Ocorreu um erro de configuração ao reautenticar.", | ||||||
|       "FACEBOOK_LOAD_ERROR": "Failed to load Facebook SDK. Please try again.", |       "FACEBOOK_LOAD_ERROR": "Falha para carregar o SDK do Facebook. Por favor, tente novamente.", | ||||||
|       "TROUBLESHOOTING": { |       "TROUBLESHOOTING": { | ||||||
|         "TITLE": "Troubleshooting", |         "TITLE": "Solucionar problemas", | ||||||
|         "POPUP_BLOCKED": "Ensure pop-ups are allowed for this site", |         "POPUP_BLOCKED": "Certifique-se de que os pop-ups são permitidos para este site", | ||||||
|         "COOKIES": "Third-party cookies must be enabled", |         "COOKIES": "_Cookies_ de terceiros devem estar habilitados", | ||||||
|         "ADMIN_ACCESS": "You need admin access to the WhatsApp Business Account" |         "ADMIN_ACCESS": "Você precisa de acesso de administrador na conta do WhatsApp Business" | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -225,13 +225,13 @@ | |||||||
|           "WHATSAPP_EMBEDDED": "WhatsApp Business", |           "WHATSAPP_EMBEDDED": "WhatsApp Business", | ||||||
|           "TWILIO": "Twilio", |           "TWILIO": "Twilio", | ||||||
|           "WHATSAPP_CLOUD": "Cloud do WhatsApp", |           "WHATSAPP_CLOUD": "Cloud do WhatsApp", | ||||||
|           "WHATSAPP_CLOUD_DESC": "Quick setup through Meta", |           "WHATSAPP_CLOUD_DESC": "Configuração rápida via Meta", | ||||||
|           "TWILIO_DESC": "Connect via Twilio credentials", |           "TWILIO_DESC": "Conectar através de credenciais Twilio", | ||||||
|           "360_DIALOG": "360Dialog" |           "360_DIALOG": "360Dialog" | ||||||
|         }, |         }, | ||||||
|         "SELECT_PROVIDER": { |         "SELECT_PROVIDER": { | ||||||
|           "TITLE": "Select your API provider", |           "TITLE": "Selecione seu provedor de API", | ||||||
|           "DESCRIPTION": "Choose your WhatsApp provider. You can connect directly through Meta which requires no setup, or connect through Twilio using your account credentials." |           "DESCRIPTION": "Escolha seu provedor do WhatsApp. Você pode se conectar diretamente através de metade, que não requer nenhuma configuração ou se conectar pelo Twilio usando as credenciais da sua conta." | ||||||
|         }, |         }, | ||||||
|         "INBOX_NAME": { |         "INBOX_NAME": { | ||||||
|           "LABEL": "Nome da Caixa de Entrada", |           "LABEL": "Nome da Caixa de Entrada", | ||||||
| @@ -272,74 +272,74 @@ | |||||||
|         }, |         }, | ||||||
|         "SUBMIT_BUTTON": "Criar canal do WhatsApp", |         "SUBMIT_BUTTON": "Criar canal do WhatsApp", | ||||||
|         "EMBEDDED_SIGNUP": { |         "EMBEDDED_SIGNUP": { | ||||||
|           "TITLE": "Quick Setup with Meta", |           "TITLE": "Configuração rápida com Meta", | ||||||
|           "DESC": "You will be redirected to Meta to log into your WhatsApp Business account. Having admin access will help make the setup smooth and easy.", |           "DESC": "Você será redirecionado para a Meta para entrar na sua conta do WhatsApp Business. Ter acesso administrativo ajudará a facilitar a instalação.", | ||||||
|           "BENEFITS": { |           "BENEFITS": { | ||||||
|             "TITLE": "Benefits of Embedded Signup:", |             "TITLE": "Benefícios da inscrição incorporada:", | ||||||
|             "EASY_SETUP": "No manual configuration required", |             "EASY_SETUP": "Nenhuma configuração manual é necessária", | ||||||
|             "SECURE_AUTH": "Secure OAuth based authentication", |             "SECURE_AUTH": "Autenticação segura baseada em OAuth", | ||||||
|             "AUTO_CONFIG": "Automatic webhook and phone number configuration" |             "AUTO_CONFIG": "Configuração automática de webhook e número de telefone" | ||||||
|           }, |           }, | ||||||
|           "LEARN_MORE": { |           "LEARN_MORE": { | ||||||
|             "TEXT": "To learn more about integrated signup, pricing, and limitations, visit", |             "TEXT": "Para saber mais sobre inscrições integradas, preços e limitações visite", | ||||||
|             "LINK_TEXT": "this link.", |             "LINK_TEXT": "este link.", | ||||||
|             "LINK_URL": "https://developers.facebook.com/docs/whatsapp/embedded-signup/custom-flows/onboarding-business-app-users#limitations" |             "LINK_URL": "https://developers.facebook.com/docs/whatsapp/embedded-signup/custom-flows/onboarding-business-app-users#limitations" | ||||||
|           }, |           }, | ||||||
|           "SUBMIT_BUTTON": "Connect with WhatsApp Business", |           "SUBMIT_BUTTON": "Conecte-se com WhatsApp Business", | ||||||
|           "AUTH_PROCESSING": "Authenticating with Meta", |           "AUTH_PROCESSING": "Autenticando com Meta", | ||||||
|           "WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...", |           "WAITING_FOR_BUSINESS_INFO": "Por favor, complete a configuração do negócio na janela da Meta...", | ||||||
|           "PROCESSING": "Setting up your WhatsApp Business Account", |           "PROCESSING": "Configurando sua conta do WhatsApp Business", | ||||||
|           "LOADING_SDK": "Loading Facebook SDK...", |           "LOADING_SDK": "Carregando SDK do Facebook...", | ||||||
|           "CANCELLED": "WhatsApp Signup was cancelled", |           "CANCELLED": "A inscrição no WhatsApp foi cancelada", | ||||||
|           "SUCCESS_TITLE": "WhatsApp Business Account Connected!", |           "SUCCESS_TITLE": "Conta do WhatsApp Business conectada!", | ||||||
|           "WAITING_FOR_AUTH": "Waiting for authentication...", |           "WAITING_FOR_AUTH": "Aguardando autenticação...", | ||||||
|           "INVALID_BUSINESS_DATA": "Invalid business data received from Facebook. Please try again.", |           "INVALID_BUSINESS_DATA": "Dados de negócio inválidos recebidos do Facebook. Por favor, tente novamente.", | ||||||
|           "SIGNUP_ERROR": "Signup error occurred", |           "SIGNUP_ERROR": "Ocorreu um erro no cadastro", | ||||||
|           "AUTH_NOT_COMPLETED": "Authentication not completed. Please restart the process.", |           "AUTH_NOT_COMPLETED": "Autenticação não concluída. Por favor, reinicie o processo.", | ||||||
|           "SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured" |           "SUCCESS_FALLBACK": "A conta do WhatsApp Business foi configurada com sucesso" | ||||||
|         }, |         }, | ||||||
|         "API": { |         "API": { | ||||||
|           "ERROR_MESSAGE": "Não foi possível salvar o canal do WhatsApp" |           "ERROR_MESSAGE": "Não foi possível salvar o canal do WhatsApp" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "VOICE": { |       "VOICE": { | ||||||
|         "TITLE": "Voice Channel", |         "TITLE": "Canal de Voz", | ||||||
|         "DESC": "Integrate Twilio Voice and start supporting your customers via phone calls.", |         "DESC": "Integre o Twilio Voice e comece a oferecer suporte a seus clientes através de chamadas telefônicas.", | ||||||
|         "PHONE_NUMBER": { |         "PHONE_NUMBER": { | ||||||
|           "LABEL": "Número de Telefone", |           "LABEL": "Número de Telefone", | ||||||
|           "PLACEHOLDER": "Enter your phone number (e.g. +1234567890)", |           "PLACEHOLDER": "Digite seu número de telefone (por exemplo, +551234567890)", | ||||||
|           "ERROR": "Please provide a valid phone number in E.164 format (e.g. +1234567890)" |           "ERROR": "Por favor, forneça um número de telefone válido no formato E.164 (por exemplo, +551234567890)" | ||||||
|         }, |         }, | ||||||
|         "TWILIO": { |         "TWILIO": { | ||||||
|           "ACCOUNT_SID": { |           "ACCOUNT_SID": { | ||||||
|             "LABEL": "SID da Conta", |             "LABEL": "SID da Conta", | ||||||
|             "PLACEHOLDER": "Enter your Twilio Account SID", |             "PLACEHOLDER": "Insira o SID da sua Conta Twilio", | ||||||
|             "REQUIRED": "Account SID is required" |             "REQUIRED": "O SID da conta é necessário" | ||||||
|           }, |           }, | ||||||
|           "AUTH_TOKEN": { |           "AUTH_TOKEN": { | ||||||
|             "LABEL": "Token de autenticação", |             "LABEL": "Token de autenticação", | ||||||
|             "PLACEHOLDER": "Enter your Twilio Auth Token", |             "PLACEHOLDER": "Por favor, digite seu Token de Autenticação do Twilio", | ||||||
|             "REQUIRED": "Auth Token is required" |             "REQUIRED": "Um Token de autenticação é necessário" | ||||||
|           }, |           }, | ||||||
|           "API_KEY_SID": { |           "API_KEY_SID": { | ||||||
|             "LABEL": "Chave da API SID", |             "LABEL": "Chave da API SID", | ||||||
|             "PLACEHOLDER": "Enter your Twilio API Key SID", |             "PLACEHOLDER": "Insira sua chave de API do Twilio SID", | ||||||
|             "REQUIRED": "API Key SID is required" |             "REQUIRED": "API Key SID é obrigatório" | ||||||
|           }, |           }, | ||||||
|           "API_KEY_SECRET": { |           "API_KEY_SECRET": { | ||||||
|             "LABEL": "Segredo da Chave API", |             "LABEL": "Segredo da Chave API", | ||||||
|             "PLACEHOLDER": "Enter your Twilio API Key Secret", |             "PLACEHOLDER": "Digite o segredo da sua chave de API do Twilio", | ||||||
|             "REQUIRED": "API Key Secret is required" |             "REQUIRED": "Segredo da chave da API é obrigatório" | ||||||
|           }, |           }, | ||||||
|           "TWIML_APP_SID": { |           "TWIML_APP_SID": { | ||||||
|             "LABEL": "TwiML App SID", |             "LABEL": "TwiML App SID", | ||||||
|             "PLACEHOLDER": "Enter your Twilio TwiML App SID (starts with AP)", |             "PLACEHOLDER": "Insira seu Twilio TwiML App SID (começa com AP)", | ||||||
|             "REQUIRED": "TwiML App SID is required" |             "REQUIRED": "TwiML App SID é obrigatório" | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         "SUBMIT_BUTTON": "Create Voice Channel", |         "SUBMIT_BUTTON": "Criar Canal de Voz", | ||||||
|         "API": { |         "API": { | ||||||
|           "ERROR_MESSAGE": "We were not able to create the voice channel" |           "ERROR_MESSAGE": "Não conseguimos criar o canal de voz" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "API_CHANNEL": { |       "API_CHANNEL": { | ||||||
| @@ -603,27 +603,27 @@ | |||||||
|       "WHATSAPP_SECTION_UPDATE_TITLE": "Atualizar Chave de API", |       "WHATSAPP_SECTION_UPDATE_TITLE": "Atualizar Chave de API", | ||||||
|       "WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Digite a nova chave de API aqui", |       "WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Digite a nova chave de API aqui", | ||||||
|       "WHATSAPP_SECTION_UPDATE_BUTTON": "Atualizar", |       "WHATSAPP_SECTION_UPDATE_BUTTON": "Atualizar", | ||||||
|       "WHATSAPP_EMBEDDED_SIGNUP_TITLE": "WhatsApp Embedded Signup", |       "WHATSAPP_EMBEDDED_SIGNUP_TITLE": "Inscrição incorporada do WhatsApp", | ||||||
|       "WHATSAPP_EMBEDDED_SIGNUP_SUBHEADER": "This inbox is connected through WhatsApp embedded signup.", |       "WHATSAPP_EMBEDDED_SIGNUP_SUBHEADER": "Esta caixa de entrada está conectada através da inscrição incorporada do WhatsApp.", | ||||||
|       "WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION": "You can reconfigure this inbox to update your WhatsApp Business settings.", |       "WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION": "Você pode reconfigurar esta caixa de entrada para atualizar suas configurações do WhatsApp Business.", | ||||||
|       "WHATSAPP_RECONFIGURE_BUTTON": "Reconfigure", |       "WHATSAPP_RECONFIGURE_BUTTON": "Reconfigurar", | ||||||
|       "WHATSAPP_CONNECT_TITLE": "Connect to WhatsApp Business", |       "WHATSAPP_CONNECT_TITLE": "Conectar ao WhatsApp Business", | ||||||
|       "WHATSAPP_CONNECT_SUBHEADER": "Upgrade to WhatsApp embedded signup for easier management.", |       "WHATSAPP_CONNECT_SUBHEADER": ".", | ||||||
|       "WHATSAPP_CONNECT_DESCRIPTION": "Connect this inbox to WhatsApp Business for enhanced features and easier management.", |       "WHATSAPP_CONNECT_DESCRIPTION": "Conecte esta caixa de entrada ao WhatsApp Business para ter recursos aprimorados e um gerenciamento mais fácil.", | ||||||
|       "WHATSAPP_CONNECT_BUTTON": "Conectar", |       "WHATSAPP_CONNECT_BUTTON": "Conectar", | ||||||
|       "WHATSAPP_CONNECT_SUCCESS": "Successfully connected to WhatsApp Business!", |       "WHATSAPP_CONNECT_SUCCESS": "Conectado com sucesso ao WhatsApp Business!", | ||||||
|       "WHATSAPP_CONNECT_ERROR": "Failed to connect to WhatsApp Business. Please try again.", |       "WHATSAPP_CONNECT_ERROR": "Não foi possível reconfigurar o WhatsApp Business. Tente novamente.", | ||||||
|       "WHATSAPP_RECONFIGURE_SUCCESS": "Successfully reconfigured WhatsApp Business!", |       "WHATSAPP_RECONFIGURE_SUCCESS": "WhatsApp Business reconfigurado com sucesso!", | ||||||
|       "WHATSAPP_RECONFIGURE_ERROR": "Failed to reconfigure WhatsApp Business. Please try again.", |       "WHATSAPP_RECONFIGURE_ERROR": "Não foi possível reconfigurar o WhatsApp Business. Tente novamente.", | ||||||
|       "WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.", |       "WHATSAPP_APP_ID_MISSING": "O ID do WhatsApp não está configurado. Por favor, contate o administrador.", | ||||||
|       "WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.", |       "WHATSAPP_CONFIG_ID_MISSING": "O ID de Configuração do WhatsApp não está configurado. Por favor, contate o administrador.", | ||||||
|       "WHATSAPP_LOGIN_CANCELLED": "WhatsApp login was cancelled. Please try again.", |       "WHATSAPP_LOGIN_CANCELLED": "O login do WhatsApp foi cancelado. Por favor, tente novamente.", | ||||||
|       "WHATSAPP_WEBHOOK_TITLE": "Token de verificação Webhook", |       "WHATSAPP_WEBHOOK_TITLE": "Token de verificação Webhook", | ||||||
|       "WHATSAPP_WEBHOOK_SUBHEADER": "Este token é usado para verificar a autenticidade do webhook endpoint.", |       "WHATSAPP_WEBHOOK_SUBHEADER": "Este token é usado para verificar a autenticidade do webhook endpoint.", | ||||||
|       "WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates", |       "WHATSAPP_TEMPLATES_SYNC_TITLE": "Sincronizar Modelos", | ||||||
|       "WHATSAPP_TEMPLATES_SYNC_SUBHEADER": "Manually sync message templates from WhatsApp to update your available templates.", |       "WHATSAPP_TEMPLATES_SYNC_SUBHEADER": "Sincronize manualmente os modelos de mensagens do WhatsApp para atualizar seus modelos disponíveis.", | ||||||
|       "WHATSAPP_TEMPLATES_SYNC_BUTTON": "Sync Templates", |       "WHATSAPP_TEMPLATES_SYNC_BUTTON": "Sincronizar Modelos", | ||||||
|       "WHATSAPP_TEMPLATES_SYNC_SUCCESS": "Templates sync initiated successfully. It may take a couple of minutes to update.", |       "WHATSAPP_TEMPLATES_SYNC_SUCCESS": "Sincronização de modelos iniciada com sucesso. Pode demorar alguns minutos para atualizar.", | ||||||
|       "UPDATE_PRE_CHAT_FORM_SETTINGS": "Atualizar configurações do Formulário Pre Chat" |       "UPDATE_PRE_CHAT_FORM_SETTINGS": "Atualizar configurações do Formulário Pre Chat" | ||||||
|     }, |     }, | ||||||
|     "HELP_CENTER": { |     "HELP_CENTER": { | ||||||
| @@ -883,7 +883,7 @@ | |||||||
|       "LINE": "Line", |       "LINE": "Line", | ||||||
|       "API": "Canal da API", |       "API": "Canal da API", | ||||||
|       "INSTAGRAM": "Instagram", |       "INSTAGRAM": "Instagram", | ||||||
|       "VOICE": "Voice" |       "VOICE": "Voz" | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -334,8 +334,8 @@ | |||||||
|     }, |     }, | ||||||
|     "NOTION": { |     "NOTION": { | ||||||
|       "DELETE": { |       "DELETE": { | ||||||
|         "TITLE": "Are you sure you want to delete the Notion integration?", |         "TITLE": "Você tem certeza que deseja excluir a integração com Notion?", | ||||||
|         "MESSAGE": "Deleting this integration will remove access to your Notion workspace and stop all related functionality.", |         "MESSAGE": "Excluir essa integração removerá o acesso ao seu espaço de trabalho Notion e encerrará todas as funcionalidades relacionadas.", | ||||||
|         "CONFIRM": "Sim, excluir", |         "CONFIRM": "Sim, excluir", | ||||||
|         "CANCEL": "Cancelar" |         "CANCEL": "Cancelar" | ||||||
|       } |       } | ||||||
| @@ -473,7 +473,7 @@ | |||||||
|           "TITLE": "Funcionalidades", |           "TITLE": "Funcionalidades", | ||||||
|           "ALLOW_CONVERSATION_FAQS": "Gerar perguntas frequentes a partir de conversas resolvidas", |           "ALLOW_CONVERSATION_FAQS": "Gerar perguntas frequentes a partir de conversas resolvidas", | ||||||
|           "ALLOW_MEMORIES": "Capture os principais detalhes como memórias de interações do cliente.", |           "ALLOW_MEMORIES": "Capture os principais detalhes como memórias de interações do cliente.", | ||||||
|           "ALLOW_CITATIONS": "Include source citations in responses" |           "ALLOW_CITATIONS": "Incluir fonte de citações nas respostas" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "EDIT": { |       "EDIT": { | ||||||
| @@ -487,28 +487,28 @@ | |||||||
|           "ASSISTANT": "Assistente" |           "ASSISTANT": "Assistente" | ||||||
|         }, |         }, | ||||||
|         "BASIC_SETTINGS": { |         "BASIC_SETTINGS": { | ||||||
|           "TITLE": "Basic settings", |           "TITLE": "Configurações básicas", | ||||||
|           "DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human." |           "DESCRIPTION": "Personalize o que o assistente diz quando termina uma conversa ou transfere para um humano." | ||||||
|         }, |         }, | ||||||
|         "SYSTEM_SETTINGS": { |         "SYSTEM_SETTINGS": { | ||||||
|           "TITLE": "System settings", |           "TITLE": "Configurações do sistema", | ||||||
|           "DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human." |           "DESCRIPTION": "Personalize o que o assistente diz quando termina uma conversa ou transfere para um humano." | ||||||
|         }, |         }, | ||||||
|         "CONTROL_ITEMS": { |         "CONTROL_ITEMS": { | ||||||
|           "TITLE": "The Fun Stuff", |           "TITLE": "As Coisas Divertidas", | ||||||
|           "DESCRIPTION": "Add more control to the assistant. (a bit more visual like a story : Query guardrail → scenarios → output) Nudges user to actually utilise these.", |           "DESCRIPTION": "Adicione mais controle ao assistente. (algo mais visual como uma história: Consulta → cenários → saída) Força o usuário para realmente utilizá-los.", | ||||||
|           "OPTIONS": { |           "OPTIONS": { | ||||||
|             "GUARDRAILS": { |             "GUARDRAILS": { | ||||||
|               "TITLE": "Guardrails", |               "TITLE": "Proteções", | ||||||
|               "DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic." |               "DESCRIPTION": "Mantém as coisas no caminho — apenas os tipos de perguntas que você quer que seu assistente responda, nada fora de limites ou fora do tópico." | ||||||
|             }, |             }, | ||||||
|             "SCENARIOS": { |             "SCENARIOS": { | ||||||
|               "TITLE": "Scenarios", |               "TITLE": "Cenários", | ||||||
|               "DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”" |               "DESCRIPTION": "Dê algum contexto ao seu assistente — como \"o que fazer quando um usuário estiver com problemas\", ou \"como agir durante uma solicitação de reembolso\"." | ||||||
|             }, |             }, | ||||||
|             "RESPONSE_GUIDELINES": { |             "RESPONSE_GUIDELINES": { | ||||||
|               "TITLE": "Response guidelines", |               "TITLE": "Diretrizes de resposta", | ||||||
|               "DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?" |               "DESCRIPTION": "O jeito e a estrutura das respostas do seu assistente — tranquilo e amigável? Curto e ágil? Detalhado e formal?" | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| @@ -527,138 +527,138 @@ | |||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "GUARDRAILS": { |       "GUARDRAILS": { | ||||||
|         "TITLE": "Guardrails", |         "TITLE": "Proteções", | ||||||
|         "DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.", |         "DESCRIPTION": "Mantém as coisas no caminho — apenas os tipos de perguntas que você quer que seu assistente responda, nada fora de limites ou fora do tópico.", | ||||||
|         "BREADCRUMB": { |         "BREADCRUMB": { | ||||||
|           "TITLE": "Guardrails" |           "TITLE": "Proteções" | ||||||
|         }, |         }, | ||||||
|         "BULK_ACTION": { |         "BULK_ACTION": { | ||||||
|           "SELECTED": "{count} item selected | {count} items selected", |           "SELECTED": "{count} item selecionado | {count} itens selecionados", | ||||||
|           "SELECT_ALL": "Selecionar todos ({count})", |           "SELECT_ALL": "Selecionar todos ({count})", | ||||||
|           "UNSELECT_ALL": "Desmarcar todos ({count})", |           "UNSELECT_ALL": "Desmarcar todos ({count})", | ||||||
|           "BULK_DELETE_BUTTON": "Excluir" |           "BULK_DELETE_BUTTON": "Excluir" | ||||||
|         }, |         }, | ||||||
|         "ADD": { |         "ADD": { | ||||||
|           "SUGGESTED": { |           "SUGGESTED": { | ||||||
|             "TITLE": "Example guardrails", |             "TITLE": "Exemplos de proteções", | ||||||
|             "ADD": "Add all", |             "ADD": "Adicionar todos", | ||||||
|             "ADD_SINGLE": "Add this", |             "ADD_SINGLE": "Adicionar este", | ||||||
|             "SAVE": "Add and save (↵)", |             "SAVE": "Adicionar e salvar (↵)", | ||||||
|             "PLACEHOLDER": "Type in another guardrail..." |             "PLACEHOLDER": "Escreva outra proteção" | ||||||
|           }, |           }, | ||||||
|           "NEW": { |           "NEW": { | ||||||
|             "TITLE": "Add a guardrail", |             "TITLE": "Adicionar proteção", | ||||||
|             "CREATE": "Criar", |             "CREATE": "Criar", | ||||||
|             "CANCEL": "Cancelar", |             "CANCEL": "Cancelar", | ||||||
|             "PLACEHOLDER": "Type in another guardrail...", |             "PLACEHOLDER": "Escreva outra proteção", | ||||||
|             "TEST_ALL": "Test all" |             "TEST_ALL": "Testar tudo" | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         "LIST": { |         "LIST": { | ||||||
|           "SEARCH_PLACEHOLDER": "Pesquisar..." |           "SEARCH_PLACEHOLDER": "Pesquisar..." | ||||||
|         }, |         }, | ||||||
|         "EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.", |         "EMPTY_MESSAGE": "Nenhuma proteção encontrada. Crie uma ou adicione exemplos para começar.", | ||||||
|         "SEARCH_EMPTY_MESSAGE": "No guardrails found for this search.", |         "SEARCH_EMPTY_MESSAGE": "Nenhuma proteção encontrada para essa pesquisa.", | ||||||
|         "API": { |         "API": { | ||||||
|           "ADD": { |           "ADD": { | ||||||
|             "SUCCESS": "Guardrails added successfully", |             "SUCCESS": "Proteções adicionadas com sucesso", | ||||||
|             "ERROR": "There was an error adding guardrails, please try again." |             "ERROR": "Ocorreu um erro ao adicionar as proteções. Por favor, tente novamente." | ||||||
|           }, |           }, | ||||||
|           "UPDATE": { |           "UPDATE": { | ||||||
|             "SUCCESS": "Guardrails updated successfully", |             "SUCCESS": "Proteções atualizados com sucesso", | ||||||
|             "ERROR": "There was an error updating guardrails, please try again." |             "ERROR": "Ocorreu um erro ao atualizar as proteções. Por favor, tente novamente." | ||||||
|           }, |           }, | ||||||
|           "DELETE": { |           "DELETE": { | ||||||
|             "SUCCESS": "Guardrails deleted successfully", |             "SUCCESS": "Proteções removidas com sucesso", | ||||||
|             "ERROR": "There was an error deleting guardrails, please try again." |             "ERROR": "Ocorreu um erro ao excluir as proteções, por favor, tente novamente." | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "RESPONSE_GUIDELINES": { |       "RESPONSE_GUIDELINES": { | ||||||
|         "TITLE": "Response Guidelines", |         "TITLE": "Diretrizes de Resposta", | ||||||
|         "DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?", |         "DESCRIPTION": "O jeito e a estrutura das respostas do seu assistente — tranquilo e amigável? Curto e ágil? Detalhado e formal?", | ||||||
|         "BREADCRUMB": { |         "BREADCRUMB": { | ||||||
|           "TITLE": "Response Guidelines" |           "TITLE": "Diretrizes de Resposta" | ||||||
|         }, |         }, | ||||||
|         "BULK_ACTION": { |         "BULK_ACTION": { | ||||||
|           "SELECTED": "{count} item selected | {count} items selected", |           "SELECTED": "{count} item selecionado | {count} itens selecionados", | ||||||
|           "SELECT_ALL": "Selecionar todos ({count})", |           "SELECT_ALL": "Selecionar todos ({count})", | ||||||
|           "UNSELECT_ALL": "Desmarcar todos ({count})", |           "UNSELECT_ALL": "Desmarcar todos ({count})", | ||||||
|           "BULK_DELETE_BUTTON": "Excluir" |           "BULK_DELETE_BUTTON": "Excluir" | ||||||
|         }, |         }, | ||||||
|         "ADD": { |         "ADD": { | ||||||
|           "SUGGESTED": { |           "SUGGESTED": { | ||||||
|             "TITLE": "Example response guidelines", |             "TITLE": "Exemplos de diretrizes de resposta", | ||||||
|             "ADD": "Add all", |             "ADD": "Adicionar todos", | ||||||
|             "ADD_SINGLE": "Add this", |             "ADD_SINGLE": "Adicionar este", | ||||||
|             "SAVE": "Add and save (↵)", |             "SAVE": "Adicionar e salvar (↵)", | ||||||
|             "PLACEHOLDER": "Type in another response guideline..." |             "PLACEHOLDER": "Escreva uma outra diretriz de resposta..." | ||||||
|           }, |           }, | ||||||
|           "NEW": { |           "NEW": { | ||||||
|             "TITLE": "Add a response guideline", |             "TITLE": "Adicione uma diretriz de resposta", | ||||||
|             "CREATE": "Criar", |             "CREATE": "Criar", | ||||||
|             "CANCEL": "Cancelar", |             "CANCEL": "Cancelar", | ||||||
|             "PLACEHOLDER": "Type in another response guideline...", |             "PLACEHOLDER": "Escreva uma outra diretriz de resposta...", | ||||||
|             "TEST_ALL": "Test all" |             "TEST_ALL": "Testar tudo" | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         "LIST": { |         "LIST": { | ||||||
|           "SEARCH_PLACEHOLDER": "Pesquisar..." |           "SEARCH_PLACEHOLDER": "Pesquisar..." | ||||||
|         }, |         }, | ||||||
|         "EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.", |         "EMPTY_MESSAGE": "Nenhuma diretriz de resposta encontrada. Crie uma ou adicione exemplos para começar.", | ||||||
|         "SEARCH_EMPTY_MESSAGE": "No response guidelines found for this search.", |         "SEARCH_EMPTY_MESSAGE": "Nenhuma diretriz de resposta encotrada para essa pesquisa.", | ||||||
|         "API": { |         "API": { | ||||||
|           "ADD": { |           "ADD": { | ||||||
|             "SUCCESS": "Response Guidelines added successfully", |             "SUCCESS": "Diretrizes de resposta adicionadas com sucesso", | ||||||
|             "ERROR": "There was an error adding response guidelines, please try again." |             "ERROR": "Houve um erro ao adicionar diretrizes de resposta, por favor, tente novamente." | ||||||
|           }, |           }, | ||||||
|           "UPDATE": { |           "UPDATE": { | ||||||
|             "SUCCESS": "Response Guidelines updated successfully", |             "SUCCESS": "Diretrizes de Resposta atualizadas com sucesso", | ||||||
|             "ERROR": "There was an error updating response guidelines, please try again." |             "ERROR": "Houve um erro ao atualizar as diretrizes de resposta, por favor, tente novamente." | ||||||
|           }, |           }, | ||||||
|           "DELETE": { |           "DELETE": { | ||||||
|             "SUCCESS": "Response Guidelines deleted successfully", |             "SUCCESS": "Diretrizes de resposta removidas com sucesso", | ||||||
|             "ERROR": "There was an error deleting response guidelines, please try again." |             "ERROR": "Houve um erro ao excluir as diretrizes de resposta, por favor, tente novamente." | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "SCENARIOS": { |       "SCENARIOS": { | ||||||
|         "TITLE": "Scenarios", |         "TITLE": "Cenários", | ||||||
|         "DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”", |         "DESCRIPTION": "Dê algum contexto ao seu assistente — como \"o que fazer quando um usuário estiver com problemas\", ou \"como agir durante uma solicitação de reembolso\".", | ||||||
|         "BREADCRUMB": { |         "BREADCRUMB": { | ||||||
|           "TITLE": "Scenarios" |           "TITLE": "Cenários" | ||||||
|         }, |         }, | ||||||
|         "BULK_ACTION": { |         "BULK_ACTION": { | ||||||
|           "SELECTED": "{count} item selected | {count} items selected", |           "SELECTED": "{count} item selecionado | {count} itens selecionados", | ||||||
|           "SELECT_ALL": "Selecionar todos ({count})", |           "SELECT_ALL": "Selecionar todos ({count})", | ||||||
|           "UNSELECT_ALL": "Desmarcar todos ({count})", |           "UNSELECT_ALL": "Desmarcar todos ({count})", | ||||||
|           "BULK_DELETE_BUTTON": "Excluir" |           "BULK_DELETE_BUTTON": "Excluir" | ||||||
|         }, |         }, | ||||||
|         "ADD": { |         "ADD": { | ||||||
|           "SUGGESTED": { |           "SUGGESTED": { | ||||||
|             "TITLE": "Example scenarios", |             "TITLE": "Exemplos de cenários", | ||||||
|             "ADD": "Add all", |             "ADD": "Adicionar todos", | ||||||
|             "ADD_SINGLE": "Add this", |             "ADD_SINGLE": "Adicionar este", | ||||||
|             "TOOLS_USED": "Tools used :" |             "TOOLS_USED": "Ferramentas usadas :" | ||||||
|           }, |           }, | ||||||
|           "NEW": { |           "NEW": { | ||||||
|             "CREATE": "Add a scenario", |             "CREATE": "Adicionar um cenário", | ||||||
|             "TITLE": "Create a scenario", |             "TITLE": "Criar um cenário", | ||||||
|             "FORM": { |             "FORM": { | ||||||
|               "TITLE": { |               "TITLE": { | ||||||
|                 "LABEL": "Título", |                 "LABEL": "Título", | ||||||
|                 "PLACEHOLDER": "Enter a name for the scenario", |                 "PLACEHOLDER": "Digite um nome para o cenário", | ||||||
|                 "ERROR": "Scenario name is required" |                 "ERROR": "O nome do cenário é obrigatório" | ||||||
|               }, |               }, | ||||||
|               "DESCRIPTION": { |               "DESCRIPTION": { | ||||||
|                 "LABEL": "Descrição", |                 "LABEL": "Descrição", | ||||||
|                 "PLACEHOLDER": "Describe how and where this scenario will be used", |                 "PLACEHOLDER": "Descreva como e onde este cenário será utilizado", | ||||||
|                 "ERROR": "Scenario description is required" |                 "ERROR": "Descrição do cenário é obrigatória" | ||||||
|               }, |               }, | ||||||
|               "INSTRUCTION": { |               "INSTRUCTION": { | ||||||
|                 "LABEL": "How to handle", |                 "LABEL": "Como lidar", | ||||||
|                 "PLACEHOLDER": "Describe how and where this scenario will be handled", |                 "PLACEHOLDER": "Descreva como e onde este cenário será utilizado", | ||||||
|                 "ERROR": "Scenario content is required" |                 "ERROR": "Conteúdo do cenário é obrigatório" | ||||||
|               }, |               }, | ||||||
|               "CREATE": "Criar", |               "CREATE": "Criar", | ||||||
|               "CANCEL": "Cancelar" |               "CANCEL": "Cancelar" | ||||||
| @@ -667,25 +667,25 @@ | |||||||
|         }, |         }, | ||||||
|         "UPDATE": { |         "UPDATE": { | ||||||
|           "CANCEL": "Cancelar", |           "CANCEL": "Cancelar", | ||||||
|           "UPDATE": "Update changes" |           "UPDATE": "Atualizar alterações" | ||||||
|         }, |         }, | ||||||
|         "LIST": { |         "LIST": { | ||||||
|           "SEARCH_PLACEHOLDER": "Pesquisar..." |           "SEARCH_PLACEHOLDER": "Pesquisar..." | ||||||
|         }, |         }, | ||||||
|         "EMPTY_MESSAGE": "No scenarios found. Create or add examples to begin.", |         "EMPTY_MESSAGE": "Nenhum cenário encontrado. Crie ou adicione exemplos para começar.", | ||||||
|         "SEARCH_EMPTY_MESSAGE": "No scenarios found for this search.", |         "SEARCH_EMPTY_MESSAGE": "Nenhum cenário encontrado para esta pesquisa.", | ||||||
|         "API": { |         "API": { | ||||||
|           "ADD": { |           "ADD": { | ||||||
|             "SUCCESS": "Scenarios added successfully", |             "SUCCESS": "Cenários adicionados com sucesso", | ||||||
|             "ERROR": "There was an error adding scenarios, please try again." |             "ERROR": "Ocorreu um erro ao adicionar cenários, por favor tente novamente." | ||||||
|           }, |           }, | ||||||
|           "UPDATE": { |           "UPDATE": { | ||||||
|             "SUCCESS": "Scenarios updated successfully", |             "SUCCESS": "Cenários atualizados com sucesso", | ||||||
|             "ERROR": "There was an error updating scenarios, please try again." |             "ERROR": "Ocorreu um erro ao atualizar cenários, por favor tente novamente." | ||||||
|           }, |           }, | ||||||
|           "DELETE": { |           "DELETE": { | ||||||
|             "SUCCESS": "Scenarios deleted successfully", |             "SUCCESS": "Cenários excluídos com sucesso", | ||||||
|             "ERROR": "There was an error deleting scenarios, please try again." |             "ERROR": "Ocorreu um erro ao excluir os cenários, por favor tente novamente." | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -3,22 +3,22 @@ | |||||||
|     "MODAL": { |     "MODAL": { | ||||||
|       "TITLE": "Templates do Whatsapp", |       "TITLE": "Templates do Whatsapp", | ||||||
|       "SUBTITLE": "Selecione o template do whatsapp que você deseja enviar", |       "SUBTITLE": "Selecione o template do whatsapp que você deseja enviar", | ||||||
|       "TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}" |       "TEMPLATE_SELECTED_SUBTITLE": "Configurar modelo: {templateName}" | ||||||
|     }, |     }, | ||||||
|     "PICKER": { |     "PICKER": { | ||||||
|       "SEARCH_PLACEHOLDER": "Pesquisar modelos", |       "SEARCH_PLACEHOLDER": "Pesquisar modelos", | ||||||
|       "NO_TEMPLATES_FOUND": "Não há templates encontrados para", |       "NO_TEMPLATES_FOUND": "Não há templates encontrados para", | ||||||
|       "HEADER": "Header", |       "HEADER": "Cabeçalho", | ||||||
|       "BODY": "Body", |       "BODY": "Corpo", | ||||||
|       "FOOTER": "Footer", |       "FOOTER": "Rodapé", | ||||||
|       "BUTTONS": "Buttons", |       "BUTTONS": "Botões", | ||||||
|       "CATEGORY": "Categoria", |       "CATEGORY": "Categoria", | ||||||
|       "MEDIA_CONTENT": "Media Content", |       "MEDIA_CONTENT": "Conteúdo de Mídia", | ||||||
|       "MEDIA_CONTENT_FALLBACK": "media content", |       "MEDIA_CONTENT_FALLBACK": "conteúdo de mídia", | ||||||
|       "NO_TEMPLATES_AVAILABLE": "No WhatsApp templates available. Click refresh to sync templates from WhatsApp.", |       "NO_TEMPLATES_AVAILABLE": "Não há modelos disponíveis do WhatsApp. Clique em atualizar para sincronizar os modelos do WhatsApp.", | ||||||
|       "REFRESH_BUTTON": "Refresh templates", |       "REFRESH_BUTTON": "Atualizar modelos", | ||||||
|       "REFRESH_SUCCESS": "Templates refresh initiated. It may take a couple of minutes to update.", |       "REFRESH_SUCCESS": "Atualização de modelos iniciada. Pode levar alguns minutos para atualizar.", | ||||||
|       "REFRESH_ERROR": "Failed to refresh templates. Please try again.", |       "REFRESH_ERROR": "Falha ao atualizar os modelos. Por favor, tente novamente.", | ||||||
|       "LABELS": { |       "LABELS": { | ||||||
|         "LANGUAGE": "Idioma", |         "LANGUAGE": "Idioma", | ||||||
|         "TEMPLATE_BODY": "Conteúdo do Template", |         "TEMPLATE_BODY": "Conteúdo do Template", | ||||||
| @@ -33,14 +33,14 @@ | |||||||
|       "GO_BACK_LABEL": "Voltar", |       "GO_BACK_LABEL": "Voltar", | ||||||
|       "SEND_MESSAGE_LABEL": "Enviar Mensagem", |       "SEND_MESSAGE_LABEL": "Enviar Mensagem", | ||||||
|       "FORM_ERROR_MESSAGE": "Por favor, preencha todas as variáveis antes de enviar", |       "FORM_ERROR_MESSAGE": "Por favor, preencha todas as variáveis antes de enviar", | ||||||
|       "MEDIA_HEADER_LABEL": "{type} Header", |       "MEDIA_HEADER_LABEL": "Cabeçalho {type}", | ||||||
|       "OTP_CODE": "Enter 4-8 digit OTP", |       "OTP_CODE": "Digite OTP de 4 a 8 dígitos", | ||||||
|       "EXPIRY_MINUTES": "Enter expiry minutes", |       "EXPIRY_MINUTES": "Digite os minutos de expiração", | ||||||
|       "BUTTON_PARAMETERS": "Button Parameters", |       "BUTTON_PARAMETERS": "Parâmetros do botão", | ||||||
|       "BUTTON_LABEL": "Button {index}", |       "BUTTON_LABEL": "Botão {index}", | ||||||
|       "COUPON_CODE": "Enter coupon code (max 15 chars)", |       "COUPON_CODE": "Digite o código do cupom (máx. 15 caracteres)", | ||||||
|       "MEDIA_URL_LABEL": "Enter {type} URL", |       "MEDIA_URL_LABEL": "Digite a URL {type}", | ||||||
|       "BUTTON_PARAMETER": "Enter button parameter" |       "BUTTON_PARAMETER": "Insira o parâmetro do botão" | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import ContactMergeModal from 'dashboard/modules/contact/ContactMergeModal.vue'; | |||||||
| import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue'; | import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue'; | ||||||
| import { BUS_EVENTS } from 'shared/constants/busEvents'; | import { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||||
| import NextButton from 'dashboard/components-next/button/Button.vue'; | import NextButton from 'dashboard/components-next/button/Button.vue'; | ||||||
|  | import VoiceCallButton from 'dashboard/components-next/Contacts/VoiceCallButton.vue'; | ||||||
|  |  | ||||||
| import { | import { | ||||||
|   isAConversationRoute, |   isAConversationRoute, | ||||||
| @@ -28,6 +29,7 @@ export default { | |||||||
|     ComposeConversation, |     ComposeConversation, | ||||||
|     SocialIcons, |     SocialIcons, | ||||||
|     ContactMergeModal, |     ContactMergeModal, | ||||||
|  |     VoiceCallButton, | ||||||
|   }, |   }, | ||||||
|   props: { |   props: { | ||||||
|     contact: { |     contact: { | ||||||
| @@ -278,6 +280,14 @@ export default { | |||||||
|             /> |             /> | ||||||
|           </template> |           </template> | ||||||
|         </ComposeConversation> |         </ComposeConversation> | ||||||
|  |         <VoiceCallButton | ||||||
|  |           :phone="contact.phone_number" | ||||||
|  |           icon="i-ri-phone-fill" | ||||||
|  |           size="sm" | ||||||
|  |           :tooltip-label="$t('CONTACT_PANEL.CALL')" | ||||||
|  |           slate | ||||||
|  |           faded | ||||||
|  |         /> | ||||||
|         <NextButton |         <NextButton | ||||||
|           v-tooltip.top-end="$t('EDIT_CONTACT.BUTTON_LABEL')" |           v-tooltip.top-end="$t('EDIT_CONTACT.BUTTON_LABEL')" | ||||||
|           icon="i-ph-pencil-simple" |           icon="i-ph-pencil-simple" | ||||||
|   | |||||||
| @@ -55,15 +55,8 @@ export default { | |||||||
|     emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.toggleReplyTo); |     emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.toggleReplyTo); | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     ...mapActions('conversation', [ |     ...mapActions('conversation', ['sendMessage', 'sendAttachment']), | ||||||
|       'sendMessage', |     ...mapActions('conversationAttributes', ['getAttributes']), | ||||||
|       'sendAttachment', |  | ||||||
|       'clearConversations', |  | ||||||
|     ]), |  | ||||||
|     ...mapActions('conversationAttributes', [ |  | ||||||
|       'getAttributes', |  | ||||||
|       'clearConversationAttributes', |  | ||||||
|     ]), |  | ||||||
|     async handleSendMessage(content) { |     async handleSendMessage(content) { | ||||||
|       await this.sendMessage({ |       await this.sendMessage({ | ||||||
|         content, |         content, | ||||||
| @@ -84,8 +77,6 @@ export default { | |||||||
|       this.inReplyTo = null; |       this.inReplyTo = null; | ||||||
|     }, |     }, | ||||||
|     startNewConversation() { |     startNewConversation() { | ||||||
|       this.clearConversations(); |  | ||||||
|       this.clearConversationAttributes(); |  | ||||||
|       this.replaceRoute('prechat-form'); |       this.replaceRoute('prechat-form'); | ||||||
|       IFrameHelper.sendMessage({ |       IFrameHelper.sendMessage({ | ||||||
|         event: 'onEvent', |         event: 'onEvent', | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ | |||||||
|   }, |   }, | ||||||
|   "THUMBNAIL": { |   "THUMBNAIL": { | ||||||
|     "AUTHOR": { |     "AUTHOR": { | ||||||
|       "NOT_AVAILABLE": "Not available" |       "NOT_AVAILABLE": "Indisponível" | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "TEAM_AVAILABILITY": { |   "TEAM_AVAILABILITY": { | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| <script> | <script> | ||||||
|  | import { mapActions } from 'vuex'; | ||||||
| import PreChatForm from '../components/PreChat/Form.vue'; | import PreChatForm from '../components/PreChat/Form.vue'; | ||||||
| import configMixin from '../mixins/configMixin'; | import configMixin from '../mixins/configMixin'; | ||||||
| import routerMixin from '../mixins/routerMixin'; | import routerMixin from '../mixins/routerMixin'; | ||||||
| @@ -19,6 +20,8 @@ export default { | |||||||
|     emitter.off(ON_CONVERSATION_CREATED, this.handleConversationCreated); |     emitter.off(ON_CONVERSATION_CREATED, this.handleConversationCreated); | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |     ...mapActions('conversation', ['clearConversations']), | ||||||
|  |     ...mapActions('conversationAttributes', ['clearConversationAttributes']), | ||||||
|     handleConversationCreated() { |     handleConversationCreated() { | ||||||
|       // Redirect to messages page after conversation is created |       // Redirect to messages page after conversation is created | ||||||
|       this.replaceRoute('messages'); |       this.replaceRoute('messages'); | ||||||
| @@ -48,6 +51,8 @@ export default { | |||||||
|           }, |           }, | ||||||
|         }); |         }); | ||||||
|       } else { |       } else { | ||||||
|  |         this.clearConversations(); | ||||||
|  |         this.clearConversationAttributes(); | ||||||
|         this.$store.dispatch('conversation/createConversation', { |         this.$store.dispatch('conversation/createConversation', { | ||||||
|           fullName: fullName, |           fullName: fullName, | ||||||
|           emailAddress: emailAddress, |           emailAddress: emailAddress, | ||||||
|   | |||||||
| @@ -61,6 +61,7 @@ class Account < ApplicationRecord | |||||||
|   has_many :agent_bots, dependent: :destroy_async |   has_many :agent_bots, dependent: :destroy_async | ||||||
|   has_many :api_channels, dependent: :destroy_async, class_name: '::Channel::Api' |   has_many :api_channels, dependent: :destroy_async, class_name: '::Channel::Api' | ||||||
|   has_many :articles, dependent: :destroy_async, class_name: '::Article' |   has_many :articles, dependent: :destroy_async, class_name: '::Article' | ||||||
|  |   has_many :assignment_policies, dependent: :destroy_async | ||||||
|   has_many :automation_rules, dependent: :destroy_async |   has_many :automation_rules, dependent: :destroy_async | ||||||
|   has_many :macros, dependent: :destroy_async |   has_many :macros, dependent: :destroy_async | ||||||
|   has_many :campaigns, dependent: :destroy_async |   has_many :campaigns, dependent: :destroy_async | ||||||
|   | |||||||
							
								
								
									
										37
									
								
								app/models/assignment_policy.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								app/models/assignment_policy.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | # == Schema Information | ||||||
|  | # | ||||||
|  | # Table name: assignment_policies | ||||||
|  | # | ||||||
|  | #  id                       :bigint           not null, primary key | ||||||
|  | #  assignment_order         :integer          default(0), not null | ||||||
|  | #  conversation_priority    :integer          default("earliest_created"), not null | ||||||
|  | #  description              :text | ||||||
|  | #  enabled                  :boolean          default(TRUE), not null | ||||||
|  | #  fair_distribution_limit  :integer          default(100), not null | ||||||
|  | #  fair_distribution_window :integer          default(3600), not null | ||||||
|  | #  name                     :string(255)      not null | ||||||
|  | #  created_at               :datetime         not null | ||||||
|  | #  updated_at               :datetime         not null | ||||||
|  | #  account_id               :bigint           not null | ||||||
|  | # | ||||||
|  | # Indexes | ||||||
|  | # | ||||||
|  | #  index_assignment_policies_on_account_id           (account_id) | ||||||
|  | #  index_assignment_policies_on_account_id_and_name  (account_id,name) UNIQUE | ||||||
|  | #  index_assignment_policies_on_enabled              (enabled) | ||||||
|  | # | ||||||
|  | class AssignmentPolicy < ApplicationRecord | ||||||
|  |   belongs_to :account | ||||||
|  |   has_many :inbox_assignment_policies, dependent: :destroy | ||||||
|  |   has_many :inboxes, through: :inbox_assignment_policies | ||||||
|  |  | ||||||
|  |   validates :name, presence: true, uniqueness: { scope: :account_id } | ||||||
|  |   validates :fair_distribution_limit, numericality: { greater_than: 0 } | ||||||
|  |   validates :fair_distribution_window, numericality: { greater_than: 0 } | ||||||
|  |  | ||||||
|  |   enum conversation_priority: { earliest_created: 0, longest_waiting: 1 } | ||||||
|  |  | ||||||
|  |   enum assignment_order: { round_robin: 0 } unless ChatwootApp.enterprise? | ||||||
|  | end | ||||||
|  |  | ||||||
|  | AssignmentPolicy.include_mod_with('Concerns::AssignmentPolicy') | ||||||
| @@ -67,6 +67,8 @@ class Inbox < ApplicationRecord | |||||||
|   has_many :conversations, dependent: :destroy_async |   has_many :conversations, dependent: :destroy_async | ||||||
|   has_many :messages, dependent: :destroy_async |   has_many :messages, dependent: :destroy_async | ||||||
|  |  | ||||||
|  |   has_one :inbox_assignment_policy, dependent: :destroy | ||||||
|  |   has_one :assignment_policy, through: :inbox_assignment_policy | ||||||
|   has_one :agent_bot_inbox, dependent: :destroy_async |   has_one :agent_bot_inbox, dependent: :destroy_async | ||||||
|   has_one :agent_bot, through: :agent_bot_inbox |   has_one :agent_bot, through: :agent_bot_inbox | ||||||
|   has_many :webhooks, dependent: :destroy_async |   has_many :webhooks, dependent: :destroy_async | ||||||
|   | |||||||
							
								
								
									
										21
									
								
								app/models/inbox_assignment_policy.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/models/inbox_assignment_policy.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | # == Schema Information | ||||||
|  | # | ||||||
|  | # Table name: inbox_assignment_policies | ||||||
|  | # | ||||||
|  | #  id                   :bigint           not null, primary key | ||||||
|  | #  created_at           :datetime         not null | ||||||
|  | #  updated_at           :datetime         not null | ||||||
|  | #  assignment_policy_id :bigint           not null | ||||||
|  | #  inbox_id             :bigint           not null | ||||||
|  | # | ||||||
|  | # Indexes | ||||||
|  | # | ||||||
|  | #  index_inbox_assignment_policies_on_assignment_policy_id  (assignment_policy_id) | ||||||
|  | #  index_inbox_assignment_policies_on_inbox_id              (inbox_id) UNIQUE | ||||||
|  | # | ||||||
|  | class InboxAssignmentPolicy < ApplicationRecord | ||||||
|  |   belongs_to :inbox | ||||||
|  |   belongs_to :assignment_policy | ||||||
|  |  | ||||||
|  |   validates :inbox_id, uniqueness: true | ||||||
|  | end | ||||||
							
								
								
									
										21
									
								
								app/policies/assignment_policy_policy.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/policies/assignment_policy_policy.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | class AssignmentPolicyPolicy < ApplicationPolicy | ||||||
|  |   def index? | ||||||
|  |     @account_user.administrator? | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def show? | ||||||
|  |     @account_user.administrator? | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def create? | ||||||
|  |     @account_user.administrator? | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def update? | ||||||
|  |     @account_user.administrator? | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def destroy? | ||||||
|  |     @account_user.administrator? | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | json.id assignment_policy.id | ||||||
|  | json.name assignment_policy.name | ||||||
|  | json.description assignment_policy.description | ||||||
|  | json.assignment_order assignment_policy.assignment_order | ||||||
|  | json.conversation_priority assignment_policy.conversation_priority | ||||||
|  | json.fair_distribution_limit assignment_policy.fair_distribution_limit | ||||||
|  | json.fair_distribution_window assignment_policy.fair_distribution_window | ||||||
|  | json.enabled assignment_policy.enabled | ||||||
|  | json.created_at assignment_policy.created_at.to_i | ||||||
|  | json.updated_at assignment_policy.updated_at.to_i | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | json.partial! 'assignment_policy', assignment_policy: @assignment_policy | ||||||
| @@ -0,0 +1,5 @@ | |||||||
|  | json.id @inbox_assignment_policy.id | ||||||
|  | json.inbox_id @inbox_assignment_policy.inbox_id | ||||||
|  | json.assignment_policy_id @inbox_assignment_policy.assignment_policy_id | ||||||
|  | json.created_at @inbox_assignment_policy.created_at.to_i | ||||||
|  | json.updated_at @inbox_assignment_policy.updated_at.to_i | ||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | json.inboxes @inboxes do |inbox| | ||||||
|  |   json.partial! 'api/v1/models/inbox', formats: [:json], resource: inbox | ||||||
|  | end | ||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | json.array! @assignment_policies do |assignment_policy| | ||||||
|  |   json.partial! 'assignment_policy', assignment_policy: assignment_policy | ||||||
|  | end | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | json.partial! 'assignment_policy', assignment_policy: @assignment_policy | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | json.partial! 'assignment_policy', assignment_policy: @assignment_policy | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | json.partial! 'api/v1/accounts/assignment_policies/assignment_policy', formats: [:json], assignment_policy: @assignment_policy | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | json.partial! 'api/v1/accounts/assignment_policies/assignment_policy', formats: [:json], assignment_policy: @assignment_policy | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| shared: &shared | shared: &shared | ||||||
|   version: '4.5.0' |   version: '4.5.1' | ||||||
|  |  | ||||||
| development: | development: | ||||||
|   <<: *shared |   <<: *shared | ||||||
|   | |||||||
| @@ -191,3 +191,7 @@ | |||||||
|   display_name: CRM V2 |   display_name: CRM V2 | ||||||
|   enabled: false |   enabled: false | ||||||
|   chatwoot_internal: true |   chatwoot_internal: true | ||||||
|  | - name: assignment_v2 | ||||||
|  |   display_name: Assignment V2 | ||||||
|  |   enabled: false | ||||||
|  |   chatwoot_internal: true | ||||||
|   | |||||||
| @@ -53,6 +53,8 @@ en: | |||||||
|       email_already_exists: 'You have already signed up for an account with %{email}' |       email_already_exists: 'You have already signed up for an account with %{email}' | ||||||
|       invalid_params: 'Invalid, please check the signup paramters and try again' |       invalid_params: 'Invalid, please check the signup paramters and try again' | ||||||
|       failed: Signup failed |       failed: Signup failed | ||||||
|  |     assignment_policy: | ||||||
|  |       not_found: Assignment policy not found | ||||||
|     data_import: |     data_import: | ||||||
|       data_type: |       data_type: | ||||||
|         invalid: Invalid data type |         invalid: Invalid data type | ||||||
|   | |||||||
| @@ -20,9 +20,9 @@ pt_BR: | |||||||
|   hello: 'Olá, mundo' |   hello: 'Olá, mundo' | ||||||
|   inbox: |   inbox: | ||||||
|     reauthorization: |     reauthorization: | ||||||
|       success: 'Channel reauthorized successfully' |       success: 'Canal reautenticado com sucesso' | ||||||
|       not_required: 'Reauthorization is not required for this inbox' |       not_required: 'Reautenticação não é necessária para esta caixa de entrada' | ||||||
|       invalid_channel: 'Invalid channel type for reauthorization' |       invalid_channel: 'Tipo de canal inválido para reautenticar' | ||||||
|   messages: |   messages: | ||||||
|     reset_password_success: Legal! A solicitação de alteração de senha foi bem sucedida. Verifique seu e-mail para obter instruções. |     reset_password_success: Legal! A solicitação de alteração de senha foi bem sucedida. Verifique seu e-mail para obter instruções. | ||||||
|     reset_password_failure: Uh ho! Não conseguimos encontrar nenhum usuário com o e-mail especificado. |     reset_password_failure: Uh ho! Não conseguimos encontrar nenhum usuário com o e-mail especificado. | ||||||
| @@ -59,12 +59,12 @@ pt_BR: | |||||||
|     slack: |     slack: | ||||||
|       invalid_channel_id: 'Canal de slack inválido. Por favor, tente novamente' |       invalid_channel_id: 'Canal de slack inválido. Por favor, tente novamente' | ||||||
|     whatsapp: |     whatsapp: | ||||||
|       token_exchange_failed: 'Failed to exchange code for access token. Please try again.' |       token_exchange_failed: 'Falha ao trocar o código por um token de acesso. Por favor, tente novamente.' | ||||||
|       invalid_token_permissions: 'The access token does not have the required permissions for WhatsApp.' |       invalid_token_permissions: 'O token de acesso não tem as permissões necessárias para o WhatsApp.' | ||||||
|       phone_info_fetch_failed: 'Failed to fetch phone number information. Please try again.' |       phone_info_fetch_failed: 'Falha ao obter a informação do número de telefone. Por favor, tente novamente.' | ||||||
|       reauthorization: |       reauthorization: | ||||||
|         generic: 'Failed to reauthorize WhatsApp. Please try again.' |         generic: 'Falha ao reautenticar o WhatsApp. Por favor, tente novamente.' | ||||||
|         not_supported: 'Reauthorization is not supported for this type of WhatsApp channel.' |         not_supported: 'Reautenticação não é suportado por este tipo de canal WhatsApp.' | ||||||
|     inboxes: |     inboxes: | ||||||
|       imap: |       imap: | ||||||
|         socket_error: Por favor, verifique a conexão de rede, endereço IMAP e tente novamente. |         socket_error: Por favor, verifique a conexão de rede, endereço IMAP e tente novamente. | ||||||
| @@ -257,8 +257,8 @@ pt_BR: | |||||||
|       description: 'Crie issues em Linear diretamente da sua janela de conversa. Alternativamente, vincule as issues lineares existentes para um processo de rastreamento de problemas mais simples e eficiente.' |       description: 'Crie issues em Linear diretamente da sua janela de conversa. Alternativamente, vincule as issues lineares existentes para um processo de rastreamento de problemas mais simples e eficiente.' | ||||||
|     notion: |     notion: | ||||||
|       name: 'Notion' |       name: 'Notion' | ||||||
|       short_description: 'Integrate databases, documents and pages directly with Captain.' |       short_description: 'Integre banco de dados, documentos e páginas diretamente com o Capitão.' | ||||||
|       description: 'Connect your Notion workspace to enable Captain to access and generate intelligent responses using content from your databases, documents, and pages to provide more contextual customer support.' |       description: 'Conecte o seu espaço de trabalho Notion para permitir que o Capitão acesse e gere respostas inteligentes usando o conteúdo de seus bancos de dados, documentos e páginas para fornecer suporte ao cliente mais contextual.' | ||||||
|     shopify: |     shopify: | ||||||
|       name: 'Shopify' |       name: 'Shopify' | ||||||
|       short_description: 'Acessar detalhes do pedido e dados de clientes da sua loja Shopify.' |       short_description: 'Acessar detalhes do pedido e dados de clientes da sua loja Shopify.' | ||||||
| @@ -359,9 +359,9 @@ pt_BR: | |||||||
|   portals: |   portals: | ||||||
|     send_instructions: |     send_instructions: | ||||||
|       email_required: 'E-mail é obrigatório' |       email_required: 'E-mail é obrigatório' | ||||||
|       invalid_email_format: 'Invalid email format' |       invalid_email_format: 'Formato inválido de e-mail' | ||||||
|       custom_domain_not_configured: 'Custom domain is not configured' |       custom_domain_not_configured: 'Domínio personalizado não está configurado' | ||||||
|       instructions_sent_successfully: 'Instructions sent successfully' |       instructions_sent_successfully: 'Instruções enviadas com sucesso' | ||||||
|       subject: 'Finish setting up %{custom_domain}' |       subject: 'Termine de configurar %{custom_domain}' | ||||||
|     ssl_status: |     ssl_status: | ||||||
|       custom_domain_not_configured: 'Custom domain is not configured' |       custom_domain_not_configured: 'Domínio personalizado não está configurado' | ||||||
|   | |||||||
| @@ -217,6 +217,15 @@ Rails.application.routes.draw do | |||||||
|             end |             end | ||||||
|           end |           end | ||||||
|  |  | ||||||
|  |           # Assignment V2 Routes | ||||||
|  |           resources :assignment_policies do | ||||||
|  |             resources :inboxes, only: [:index, :create, :destroy], module: :assignment_policies | ||||||
|  |           end | ||||||
|  |  | ||||||
|  |           resources :inboxes, only: [] do | ||||||
|  |             resource :assignment_policy, only: [:show, :create, :destroy], module: :inboxes | ||||||
|  |           end | ||||||
|  |  | ||||||
|           namespace :twitter do |           namespace :twitter do | ||||||
|             resource :authorization, only: [:create] |             resource :authorization, only: [:create] | ||||||
|           end |           end | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| class AddFeatureCitationToAssistantConfig < ActiveRecord::Migration[7.1] | class AddFeatureCitationToAssistantConfig < ActiveRecord::Migration[7.1] | ||||||
|   def up |   def up | ||||||
|  |     return unless ChatwootApp.enterprise? | ||||||
|  |  | ||||||
|     Captain::Assistant.find_each do |assistant| |     Captain::Assistant.find_each do |assistant| | ||||||
|       assistant.update!( |       assistant.update!( | ||||||
|         config: assistant.config.merge('feature_citation' => true) |         config: assistant.config.merge('feature_citation' => true) | ||||||
| @@ -8,6 +10,8 @@ class AddFeatureCitationToAssistantConfig < ActiveRecord::Migration[7.1] | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   def down |   def down | ||||||
|  |     return unless ChatwootApp.enterprise? | ||||||
|  |  | ||||||
|     Captain::Assistant.find_each do |assistant| |     Captain::Assistant.find_each do |assistant| | ||||||
|       config = assistant.config.dup |       config = assistant.config.dup | ||||||
|       config.delete('feature_citation') |       config.delete('feature_citation') | ||||||
|   | |||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | module Enterprise::Concerns::AssignmentPolicy | ||||||
|  |   extend ActiveSupport::Concern | ||||||
|  |  | ||||||
|  |   included do | ||||||
|  |     enum assignment_order: { round_robin: 0, balanced: 1 } if ChatwootApp.enterprise? | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "@chatwoot/chatwoot", |   "name": "@chatwoot/chatwoot", | ||||||
|   "version": "4.5.0", |   "version": "4.5.1", | ||||||
|   "license": "MIT", |   "license": "MIT", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "eslint": "eslint app/**/*.{js,vue}", |     "eslint": "eslint app/**/*.{js,vue}", | ||||||
|   | |||||||
| @@ -0,0 +1,63 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  |  | ||||||
|  | RSpec.describe 'Assignment Policy Inboxes API', type: :request do | ||||||
|  |   let(:account) { create(:account) } | ||||||
|  |   let(:assignment_policy) { create(:assignment_policy, account: account) } | ||||||
|  |  | ||||||
|  |   describe 'GET /api/v1/accounts/{account_id}/assignment_policies/{assignment_policy_id}/inboxes' do | ||||||
|  |     context 'when it is an unauthenticated user' do | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}/inboxes" | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an authenticated admin' do | ||||||
|  |       let(:admin) { create(:user, account: account, role: :administrator) } | ||||||
|  |  | ||||||
|  |       context 'when assignment policy has associated inboxes' do | ||||||
|  |         before do | ||||||
|  |           inbox1 = create(:inbox, account: account) | ||||||
|  |           inbox2 = create(:inbox, account: account) | ||||||
|  |           create(:inbox_assignment_policy, inbox: inbox1, assignment_policy: assignment_policy) | ||||||
|  |           create(:inbox_assignment_policy, inbox: inbox2, assignment_policy: assignment_policy) | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         it 'returns all inboxes associated with the assignment policy' do | ||||||
|  |           get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}/inboxes", | ||||||
|  |               headers: admin.create_new_auth_token, | ||||||
|  |               as: :json | ||||||
|  |  | ||||||
|  |           expect(response).to have_http_status(:success) | ||||||
|  |           json_response = response.parsed_body | ||||||
|  |           expect(json_response['inboxes']).to be_an(Array) | ||||||
|  |           expect(json_response['inboxes'].length).to eq(2) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       context 'when assignment policy has no associated inboxes' do | ||||||
|  |         it 'returns empty array' do | ||||||
|  |           get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}/inboxes", | ||||||
|  |               headers: admin.create_new_auth_token, | ||||||
|  |               as: :json | ||||||
|  |  | ||||||
|  |           expect(response).to have_http_status(:success) | ||||||
|  |           json_response = response.parsed_body | ||||||
|  |           expect(json_response['inboxes']).to eq([]) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an agent' do | ||||||
|  |       let(:agent) { create(:user, account: account, role: :agent) } | ||||||
|  |  | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}/inboxes", | ||||||
|  |             headers: agent.create_new_auth_token, | ||||||
|  |             as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -0,0 +1,326 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  |  | ||||||
|  | RSpec.describe 'Assignment Policies API', type: :request do | ||||||
|  |   let(:account) { create(:account) } | ||||||
|  |  | ||||||
|  |   describe 'GET /api/v1/accounts/{account.id}/assignment_policies' do | ||||||
|  |     context 'when it is an unauthenticated user' do | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         get "/api/v1/accounts/#{account.id}/assignment_policies" | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an authenticated admin' do | ||||||
|  |       let(:admin) { create(:user, account: account, role: :administrator) } | ||||||
|  |  | ||||||
|  |       before do | ||||||
|  |         create_list(:assignment_policy, 3, account: account) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'returns all assignment policies for the account' do | ||||||
|  |         get "/api/v1/accounts/#{account.id}/assignment_policies", | ||||||
|  |             headers: admin.create_new_auth_token, | ||||||
|  |             as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:success) | ||||||
|  |         json_response = response.parsed_body | ||||||
|  |         expect(json_response.length).to eq(3) | ||||||
|  |         expect(json_response.first.keys).to include('id', 'name', 'description') | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an agent' do | ||||||
|  |       let(:agent) { create(:user, account: account, role: :agent) } | ||||||
|  |  | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         get "/api/v1/accounts/#{account.id}/assignment_policies", | ||||||
|  |             headers: agent.create_new_auth_token, | ||||||
|  |             as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe 'GET /api/v1/accounts/{account.id}/assignment_policies/:id' do | ||||||
|  |     let(:assignment_policy) { create(:assignment_policy, account: account) } | ||||||
|  |  | ||||||
|  |     context 'when it is an unauthenticated user' do | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}" | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an authenticated admin' do | ||||||
|  |       let(:admin) { create(:user, account: account, role: :administrator) } | ||||||
|  |  | ||||||
|  |       it 'returns the assignment policy' do | ||||||
|  |         get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", | ||||||
|  |             headers: admin.create_new_auth_token, | ||||||
|  |             as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:success) | ||||||
|  |         json_response = response.parsed_body | ||||||
|  |         expect(json_response['id']).to eq(assignment_policy.id) | ||||||
|  |         expect(json_response['name']).to eq(assignment_policy.name) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'returns not found for non-existent policy' do | ||||||
|  |         get "/api/v1/accounts/#{account.id}/assignment_policies/999999", | ||||||
|  |             headers: admin.create_new_auth_token, | ||||||
|  |             as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:not_found) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an agent' do | ||||||
|  |       let(:agent) { create(:user, account: account, role: :agent) } | ||||||
|  |  | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", | ||||||
|  |             headers: agent.create_new_auth_token, | ||||||
|  |             as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe 'POST /api/v1/accounts/{account.id}/assignment_policies' do | ||||||
|  |     let(:valid_params) do | ||||||
|  |       { | ||||||
|  |         assignment_policy: { | ||||||
|  |           name: 'New Assignment Policy', | ||||||
|  |           description: 'Policy for new team', | ||||||
|  |           conversation_priority: 'longest_waiting', | ||||||
|  |           fair_distribution_limit: 15, | ||||||
|  |           enabled: true | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an unauthenticated user' do | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         post "/api/v1/accounts/#{account.id}/assignment_policies", params: valid_params | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an authenticated admin' do | ||||||
|  |       let(:admin) { create(:user, account: account, role: :administrator) } | ||||||
|  |  | ||||||
|  |       it 'creates a new assignment policy' do | ||||||
|  |         expect do | ||||||
|  |           post "/api/v1/accounts/#{account.id}/assignment_policies", | ||||||
|  |                headers: admin.create_new_auth_token, | ||||||
|  |                params: valid_params, | ||||||
|  |                as: :json | ||||||
|  |         end.to change(AssignmentPolicy, :count).by(1) | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:success) | ||||||
|  |         json_response = response.parsed_body | ||||||
|  |         expect(json_response['name']).to eq('New Assignment Policy') | ||||||
|  |         expect(json_response['conversation_priority']).to eq('longest_waiting') | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'creates policy with minimal required params' do | ||||||
|  |         minimal_params = { assignment_policy: { name: 'Minimal Policy' } } | ||||||
|  |  | ||||||
|  |         expect do | ||||||
|  |           post "/api/v1/accounts/#{account.id}/assignment_policies", | ||||||
|  |                headers: admin.create_new_auth_token, | ||||||
|  |                params: minimal_params, | ||||||
|  |                as: :json | ||||||
|  |         end.to change(AssignmentPolicy, :count).by(1) | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:success) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'prevents duplicate policy names within account' do | ||||||
|  |         create(:assignment_policy, account: account, name: 'Duplicate Policy') | ||||||
|  |         duplicate_params = { assignment_policy: { name: 'Duplicate Policy' } } | ||||||
|  |  | ||||||
|  |         expect do | ||||||
|  |           post "/api/v1/accounts/#{account.id}/assignment_policies", | ||||||
|  |                headers: admin.create_new_auth_token, | ||||||
|  |                params: duplicate_params, | ||||||
|  |                as: :json | ||||||
|  |         end.not_to change(AssignmentPolicy, :count) | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unprocessable_entity) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'validates required fields' do | ||||||
|  |         invalid_params = { assignment_policy: { name: '' } } | ||||||
|  |  | ||||||
|  |         post "/api/v1/accounts/#{account.id}/assignment_policies", | ||||||
|  |              headers: admin.create_new_auth_token, | ||||||
|  |              params: invalid_params, | ||||||
|  |              as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unprocessable_entity) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an agent' do | ||||||
|  |       let(:agent) { create(:user, account: account, role: :agent) } | ||||||
|  |  | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         post "/api/v1/accounts/#{account.id}/assignment_policies", | ||||||
|  |              headers: agent.create_new_auth_token, | ||||||
|  |              params: valid_params, | ||||||
|  |              as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe 'PUT /api/v1/accounts/{account.id}/assignment_policies/:id' do | ||||||
|  |     let(:assignment_policy) { create(:assignment_policy, account: account, name: 'Original Policy') } | ||||||
|  |     let(:update_params) do | ||||||
|  |       { | ||||||
|  |         assignment_policy: { | ||||||
|  |           name: 'Updated Policy', | ||||||
|  |           description: 'Updated description', | ||||||
|  |           fair_distribution_limit: 20 | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an unauthenticated user' do | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         put "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", | ||||||
|  |             params: update_params | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an authenticated admin' do | ||||||
|  |       let(:admin) { create(:user, account: account, role: :administrator) } | ||||||
|  |  | ||||||
|  |       it 'updates the assignment policy' do | ||||||
|  |         put "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", | ||||||
|  |             headers: admin.create_new_auth_token, | ||||||
|  |             params: update_params, | ||||||
|  |             as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:success) | ||||||
|  |         assignment_policy.reload | ||||||
|  |         expect(assignment_policy.name).to eq('Updated Policy') | ||||||
|  |         expect(assignment_policy.fair_distribution_limit).to eq(20) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'allows partial updates' do | ||||||
|  |         partial_params = { assignment_policy: { enabled: false } } | ||||||
|  |  | ||||||
|  |         put "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", | ||||||
|  |             headers: admin.create_new_auth_token, | ||||||
|  |             params: partial_params, | ||||||
|  |             as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:success) | ||||||
|  |         expect(assignment_policy.reload.enabled).to be(false) | ||||||
|  |         expect(assignment_policy.name).to eq('Original Policy') # unchanged | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'prevents duplicate names during update' do | ||||||
|  |         create(:assignment_policy, account: account, name: 'Existing Policy') | ||||||
|  |         duplicate_params = { assignment_policy: { name: 'Existing Policy' } } | ||||||
|  |  | ||||||
|  |         put "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", | ||||||
|  |             headers: admin.create_new_auth_token, | ||||||
|  |             params: duplicate_params, | ||||||
|  |             as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unprocessable_entity) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'returns not found for non-existent policy' do | ||||||
|  |         put "/api/v1/accounts/#{account.id}/assignment_policies/999999", | ||||||
|  |             headers: admin.create_new_auth_token, | ||||||
|  |             params: update_params, | ||||||
|  |             as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:not_found) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an agent' do | ||||||
|  |       let(:agent) { create(:user, account: account, role: :agent) } | ||||||
|  |  | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         put "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", | ||||||
|  |             headers: agent.create_new_auth_token, | ||||||
|  |             params: update_params, | ||||||
|  |             as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe 'DELETE /api/v1/accounts/{account.id}/assignment_policies/:id' do | ||||||
|  |     let(:assignment_policy) { create(:assignment_policy, account: account) } | ||||||
|  |  | ||||||
|  |     context 'when it is an unauthenticated user' do | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         delete "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}" | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an authenticated admin' do | ||||||
|  |       let(:admin) { create(:user, account: account, role: :administrator) } | ||||||
|  |  | ||||||
|  |       it 'deletes the assignment policy' do | ||||||
|  |         assignment_policy # create it first | ||||||
|  |  | ||||||
|  |         expect do | ||||||
|  |           delete "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", | ||||||
|  |                  headers: admin.create_new_auth_token, | ||||||
|  |                  as: :json | ||||||
|  |         end.to change(AssignmentPolicy, :count).by(-1) | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:ok) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'cascades deletion to associated inbox assignment policies' do | ||||||
|  |         inbox = create(:inbox, account: account) | ||||||
|  |         create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy) | ||||||
|  |  | ||||||
|  |         expect do | ||||||
|  |           delete "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", | ||||||
|  |                  headers: admin.create_new_auth_token, | ||||||
|  |                  as: :json | ||||||
|  |         end.to change(InboxAssignmentPolicy, :count).by(-1) | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:ok) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'returns not found for non-existent policy' do | ||||||
|  |         delete "/api/v1/accounts/#{account.id}/assignment_policies/999999", | ||||||
|  |                headers: admin.create_new_auth_token, | ||||||
|  |                as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:not_found) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an agent' do | ||||||
|  |       let(:agent) { create(:user, account: account, role: :agent) } | ||||||
|  |  | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         delete "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}", | ||||||
|  |                headers: agent.create_new_auth_token, | ||||||
|  |                as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -0,0 +1,195 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  |  | ||||||
|  | RSpec.describe 'Inbox Assignment Policies API', type: :request do | ||||||
|  |   let(:account) { create(:account) } | ||||||
|  |   let(:inbox) { create(:inbox, account: account) } | ||||||
|  |   let(:assignment_policy) { create(:assignment_policy, account: account) } | ||||||
|  |  | ||||||
|  |   describe 'GET /api/v1/accounts/{account_id}/inboxes/{inbox_id}/assignment_policy' do | ||||||
|  |     context 'when it is an unauthenticated user' do | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy" | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an authenticated admin' do | ||||||
|  |       let(:admin) { create(:user, account: account, role: :administrator) } | ||||||
|  |  | ||||||
|  |       context 'when inbox has an assignment policy' do | ||||||
|  |         before do | ||||||
|  |           create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy) | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         it 'returns the assignment policy for the inbox' do | ||||||
|  |           get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", | ||||||
|  |               headers: admin.create_new_auth_token, | ||||||
|  |               as: :json | ||||||
|  |  | ||||||
|  |           expect(response).to have_http_status(:success) | ||||||
|  |           json_response = response.parsed_body | ||||||
|  |           expect(json_response['id']).to eq(assignment_policy.id) | ||||||
|  |           expect(json_response['name']).to eq(assignment_policy.name) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       context 'when inbox has no assignment policy' do | ||||||
|  |         it 'returns not found' do | ||||||
|  |           get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", | ||||||
|  |               headers: admin.create_new_auth_token, | ||||||
|  |               as: :json | ||||||
|  |  | ||||||
|  |           expect(response).to have_http_status(:not_found) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an agent' do | ||||||
|  |       let(:agent) { create(:user, account: account, role: :agent) } | ||||||
|  |  | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", | ||||||
|  |             headers: agent.create_new_auth_token, | ||||||
|  |             as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe 'POST /api/v1/accounts/{account_id}/inboxes/{inbox_id}/assignment_policy' do | ||||||
|  |     context 'when it is an unauthenticated user' do | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", | ||||||
|  |              params: { assignment_policy_id: assignment_policy.id } | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an authenticated admin' do | ||||||
|  |       let(:admin) { create(:user, account: account, role: :administrator) } | ||||||
|  |  | ||||||
|  |       it 'assigns a policy to the inbox' do | ||||||
|  |         expect do | ||||||
|  |           post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", | ||||||
|  |                params: { assignment_policy_id: assignment_policy.id }, | ||||||
|  |                headers: admin.create_new_auth_token, | ||||||
|  |                as: :json | ||||||
|  |         end.to change(InboxAssignmentPolicy, :count).by(1) | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:success) | ||||||
|  |         json_response = response.parsed_body | ||||||
|  |         expect(json_response['id']).to eq(assignment_policy.id) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'replaces existing assignment policy for inbox' do | ||||||
|  |         other_policy = create(:assignment_policy, account: account) | ||||||
|  |         create(:inbox_assignment_policy, inbox: inbox, assignment_policy: other_policy) | ||||||
|  |  | ||||||
|  |         expect do | ||||||
|  |           post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", | ||||||
|  |                params: { assignment_policy_id: assignment_policy.id }, | ||||||
|  |                headers: admin.create_new_auth_token, | ||||||
|  |                as: :json | ||||||
|  |         end.not_to change(InboxAssignmentPolicy, :count) | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:success) | ||||||
|  |         expect(inbox.reload.inbox_assignment_policy.assignment_policy).to eq(assignment_policy) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'returns not found for invalid assignment policy' do | ||||||
|  |         post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", | ||||||
|  |              params: { assignment_policy_id: 999_999 }, | ||||||
|  |              headers: admin.create_new_auth_token, | ||||||
|  |              as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:not_found) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'returns not found for invalid inbox' do | ||||||
|  |         post "/api/v1/accounts/#{account.id}/inboxes/999999/assignment_policy", | ||||||
|  |              params: { assignment_policy_id: assignment_policy.id }, | ||||||
|  |              headers: admin.create_new_auth_token, | ||||||
|  |              as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:not_found) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an agent' do | ||||||
|  |       let(:agent) { create(:user, account: account, role: :agent) } | ||||||
|  |  | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", | ||||||
|  |              params: { assignment_policy_id: assignment_policy.id }, | ||||||
|  |              headers: agent.create_new_auth_token, | ||||||
|  |              as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe 'DELETE /api/v1/accounts/{account_id}/inboxes/{inbox_id}/assignment_policy' do | ||||||
|  |     context 'when it is an unauthenticated user' do | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         delete "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy" | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an authenticated admin' do | ||||||
|  |       let(:admin) { create(:user, account: account, role: :administrator) } | ||||||
|  |  | ||||||
|  |       context 'when inbox has an assignment policy' do | ||||||
|  |         before do | ||||||
|  |           create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy) | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         it 'removes the assignment policy from inbox' do | ||||||
|  |           expect do | ||||||
|  |             delete "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", | ||||||
|  |                    headers: admin.create_new_auth_token, | ||||||
|  |                    as: :json | ||||||
|  |           end.to change(InboxAssignmentPolicy, :count).by(-1) | ||||||
|  |  | ||||||
|  |           expect(response).to have_http_status(:success) | ||||||
|  |           expect(inbox.reload.inbox_assignment_policy).to be_nil | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       context 'when inbox has no assignment policy' do | ||||||
|  |         it 'returns error' do | ||||||
|  |           expect do | ||||||
|  |             delete "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", | ||||||
|  |                    headers: admin.create_new_auth_token, | ||||||
|  |                    as: :json | ||||||
|  |           end.not_to change(InboxAssignmentPolicy, :count) | ||||||
|  |  | ||||||
|  |           expect(response).to have_http_status(:not_found) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'returns not found for invalid inbox' do | ||||||
|  |         delete "/api/v1/accounts/#{account.id}/inboxes/999999/assignment_policy", | ||||||
|  |                headers: admin.create_new_auth_token, | ||||||
|  |                as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:not_found) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an agent' do | ||||||
|  |       let(:agent) { create(:user, account: account, role: :agent) } | ||||||
|  |  | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         delete "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy", | ||||||
|  |                headers: agent.create_new_auth_token, | ||||||
|  |                as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										18
									
								
								spec/enterprise/models/assignment_policy_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								spec/enterprise/models/assignment_policy_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  |  | ||||||
|  | RSpec.describe AssignmentPolicy do | ||||||
|  |   let(:account) { create(:account) } | ||||||
|  |  | ||||||
|  |   describe 'enum values' do | ||||||
|  |     let(:assignment_policy) { create(:assignment_policy, account: account) } | ||||||
|  |  | ||||||
|  |     describe 'assignment_order' do | ||||||
|  |       it 'can be set to balanced' do | ||||||
|  |         assignment_policy.update!(assignment_order: :balanced) | ||||||
|  |         expect(assignment_policy.assignment_order).to eq('balanced') | ||||||
|  |         expect(assignment_policy.round_robin?).to be false | ||||||
|  |         expect(assignment_policy.balanced?).to be true | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										12
									
								
								spec/factories/assignment_policies.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								spec/factories/assignment_policies.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | FactoryBot.define do | ||||||
|  |   factory :assignment_policy do | ||||||
|  |     account | ||||||
|  |     sequence(:name) { |n| "Assignment Policy #{n}" } | ||||||
|  |     description { 'Test assignment policy description' } | ||||||
|  |     assignment_order { 0 } | ||||||
|  |     conversation_priority { 0 } | ||||||
|  |     fair_distribution_limit { 10 } | ||||||
|  |     fair_distribution_window { 3600 } | ||||||
|  |     enabled { true } | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										6
									
								
								spec/factories/inbox_assignment_policies.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								spec/factories/inbox_assignment_policies.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | FactoryBot.define do | ||||||
|  |   factory :inbox_assignment_policy do | ||||||
|  |     inbox | ||||||
|  |     assignment_policy | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										56
									
								
								spec/models/assignment_policy_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								spec/models/assignment_policy_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  |  | ||||||
|  | RSpec.describe AssignmentPolicy do | ||||||
|  |   describe 'associations' do | ||||||
|  |     it { is_expected.to belong_to(:account) } | ||||||
|  |     it { is_expected.to have_many(:inbox_assignment_policies).dependent(:destroy) } | ||||||
|  |     it { is_expected.to have_many(:inboxes).through(:inbox_assignment_policies) } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe 'validations' do | ||||||
|  |     subject { build(:assignment_policy) } | ||||||
|  |  | ||||||
|  |     it { is_expected.to validate_presence_of(:name) } | ||||||
|  |     it { is_expected.to validate_uniqueness_of(:name).scoped_to(:account_id) } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe 'fair distribution validations' do | ||||||
|  |     it 'requires fair_distribution_limit to be greater than 0' do | ||||||
|  |       policy = build(:assignment_policy, fair_distribution_limit: 0) | ||||||
|  |       expect(policy).not_to be_valid | ||||||
|  |       expect(policy.errors[:fair_distribution_limit]).to include('must be greater than 0') | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     it 'requires fair_distribution_window to be greater than 0' do | ||||||
|  |       policy = build(:assignment_policy, fair_distribution_window: -1) | ||||||
|  |       expect(policy).not_to be_valid | ||||||
|  |       expect(policy.errors[:fair_distribution_window]).to include('must be greater than 0') | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe 'enum values' do | ||||||
|  |     let(:assignment_policy) { create(:assignment_policy) } | ||||||
|  |  | ||||||
|  |     describe 'conversation_priority' do | ||||||
|  |       it 'can be set to earliest_created' do | ||||||
|  |         assignment_policy.update!(conversation_priority: :earliest_created) | ||||||
|  |         expect(assignment_policy.conversation_priority).to eq('earliest_created') | ||||||
|  |         expect(assignment_policy.earliest_created?).to be true | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'can be set to longest_waiting' do | ||||||
|  |         assignment_policy.update!(conversation_priority: :longest_waiting) | ||||||
|  |         expect(assignment_policy.conversation_priority).to eq('longest_waiting') | ||||||
|  |         expect(assignment_policy.longest_waiting?).to be true | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     describe 'assignment_order' do | ||||||
|  |       it 'can be set to round_robin' do | ||||||
|  |         assignment_policy.update!(assignment_order: :round_robin) | ||||||
|  |         expect(assignment_policy.assignment_order).to eq('round_robin') | ||||||
|  |         expect(assignment_policy.round_robin?).to be true | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
		Reference in New Issue
	
	Block a user
	 Sojan Jose
					Sojan Jose