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 Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue'; | ||||
| import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue'; | ||||
| import VoiceCallButton from 'dashboard/components-next/Contacts/VoiceCallButton.vue'; | ||||
|  | ||||
| const props = defineProps({ | ||||
|   selectedContact: { | ||||
| @@ -99,6 +100,11 @@ const closeMobileSidebar = () => { | ||||
|                 :disabled="isUpdating" | ||||
|                 @click="toggleBlock" | ||||
|               /> | ||||
|               <VoiceCallButton | ||||
|                 :phone="selectedContact?.phoneNumber" | ||||
|                 :label="$t('CONTACT_PANEL.CALL')" | ||||
|                 size="sm" | ||||
|               /> | ||||
|               <ComposeConversation :contact-id="contactId"> | ||||
|                 <template #trigger="{ toggle }"> | ||||
|                   <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", | ||||
|     "CREATED_AT_LABEL": "Created", | ||||
|     "NEW_MESSAGE": "New message", | ||||
|     "CALL": "Call", | ||||
|     "CALL_UNDER_DEVELOPMENT": "Calling is under development", | ||||
|     "VOICE_INBOX_PICKER": { | ||||
|       "TITLE": "Choose a voice inbox" | ||||
|     }, | ||||
|     "CONVERSATIONS": { | ||||
|       "NO_RECORDS_FOUND": "There are no previous conversations associated to this contact.", | ||||
|       "TITLE": "Previous Conversations" | ||||
|   | ||||
| @@ -13,7 +13,7 @@ | ||||
|       "TEXT": "Texto", | ||||
|       "NUMBER": "Número", | ||||
|       "LINK": "Link", | ||||
|       "DATE": "Date", | ||||
|       "DATE": "Data", | ||||
|       "LIST": "Lista", | ||||
|       "CHECKBOX": "Checkbox" | ||||
|     }, | ||||
|   | ||||
| @@ -131,7 +131,7 @@ | ||||
|       "CONVERSATION_CREATED": "Conversa Criada", | ||||
|       "CONVERSATION_UPDATED": "Conversa Atualizada", | ||||
|       "MESSAGE_CREATED": "Mensagem Criada", | ||||
|       "CONVERSATION_RESOLVED": "Conversation Resolved", | ||||
|       "CONVERSATION_RESOLVED": "Conversa resolvida", | ||||
|       "CONVERSATION_OPENED": "Conversa Aberta" | ||||
|     }, | ||||
|     "ACTIONS": { | ||||
| @@ -153,8 +153,8 @@ | ||||
|       "OPEN_CONVERSATION": "Abrir conversa" | ||||
|     }, | ||||
|     "MESSAGE_TYPES": { | ||||
|       "INCOMING": "Incoming Message", | ||||
|       "OUTGOING": "Outgoing Message" | ||||
|       "INCOMING": "Mensagem Recebida", | ||||
|       "OUTGOING": "Mensagem de Saída" | ||||
|     }, | ||||
|     "PRIORITY_TYPES": { | ||||
|       "NONE": "Nenhuma", | ||||
|   | ||||
| @@ -138,11 +138,11 @@ | ||||
|       } | ||||
|     }, | ||||
|     "WHATSAPP": { | ||||
|       "HEADER_TITLE": "WhatsApp campaigns", | ||||
|       "HEADER_TITLE": "Campanhas do WhatsApp", | ||||
|       "NEW_CAMPAIGN": "Criar campanha", | ||||
|       "EMPTY_STATE": { | ||||
|         "TITLE": "No WhatsApp campaigns are available", | ||||
|         "SUBTITLE": "Launch a WhatsApp campaign to reach your customers directly. Send offers or make announcements with ease. Click 'Create campaign' to get started." | ||||
|         "TITLE": "Nenhuma campanha do WhatsApp está disponível", | ||||
|         "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": { | ||||
|         "STATUS": { | ||||
| @@ -155,7 +155,7 @@ | ||||
|         } | ||||
|       }, | ||||
|       "CREATE": { | ||||
|         "TITLE": "Create WhatsApp campaign", | ||||
|         "TITLE": "Criar campanha do WhatsApp", | ||||
|         "CANCEL_BUTTON_TEXT": "Cancelar", | ||||
|         "CREATE_BUTTON_TEXT": "Criar", | ||||
|         "FORM": { | ||||
| @@ -170,15 +170,15 @@ | ||||
|             "ERROR": "Caixa de entrada obrigatória" | ||||
|           }, | ||||
|           "TEMPLATE": { | ||||
|             "LABEL": "WhatsApp Template", | ||||
|             "PLACEHOLDER": "Select a template", | ||||
|             "INFO": "Select a template to use for this campaign.", | ||||
|             "ERROR": "Template is required", | ||||
|             "LABEL": "Modelo do WhatsApp", | ||||
|             "PLACEHOLDER": "Selecione um modelo", | ||||
|             "INFO": "Selecione um modelo para usar para esta campanha.", | ||||
|             "ERROR": "Modelo é obrigatório", | ||||
|             "PREVIEW_TITLE": "Processar {templateName}", | ||||
|             "LANGUAGE": "Idioma", | ||||
|             "CATEGORY": "Categoria", | ||||
|             "VARIABLES_LABEL": "Variáveis", | ||||
|             "VARIABLE_PLACEHOLDER": "Enter value for {variable}" | ||||
|             "VARIABLE_PLACEHOLDER": "Digite um valor para {variable}" | ||||
|           }, | ||||
|           "AUDIENCE": { | ||||
|             "LABEL": "Público", | ||||
| @@ -195,7 +195,7 @@ | ||||
|             "CANCEL": "Cancelar" | ||||
|           }, | ||||
|           "API": { | ||||
|             "SUCCESS_MESSAGE": "WhatsApp campaign created successfully", | ||||
|             "SUCCESS_MESSAGE": "Campanha do WhatsApp criada com sucesso", | ||||
|             "ERROR_MESSAGE": "Houve um erro. Por favor, tente novamente." | ||||
|           } | ||||
|         } | ||||
|   | ||||
| @@ -51,6 +51,6 @@ | ||||
|     "PLACEHOLDER": "Insira a duração" | ||||
|   }, | ||||
|   "CHANNEL_SELECTOR": { | ||||
|     "COMING_SOON": "Coming Soon!" | ||||
|     "COMING_SOON": "Em breve!" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -144,9 +144,9 @@ | ||||
|       "AGENTS_LOADING": "Carregando agentes...", | ||||
|       "ASSIGN_TEAM": "Atribuir time", | ||||
|       "DELETE": "Excluir conversa", | ||||
|       "OPEN_IN_NEW_TAB": "Open in new tab", | ||||
|       "COPY_LINK": "Copy conversation link", | ||||
|       "COPY_LINK_SUCCESS": "Conversation link copied to clipboard", | ||||
|       "OPEN_IN_NEW_TAB": "Abrir em nova aba", | ||||
|       "COPY_LINK": "Copiar link da conversa", | ||||
|       "COPY_LINK_SUCCESS": "Link da conversa copiado", | ||||
|       "API": { | ||||
|         "AGENT_ASSIGNMENT": { | ||||
|           "SUCCESFUL": "ID da conversa {conversationId} atribuído para \"{agentName}\"", | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|     "LIMIT_MESSAGES": { | ||||
|       "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.", | ||||
|       "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." | ||||
|     }, | ||||
|     "TITLE": "Conta", | ||||
| @@ -134,7 +134,7 @@ | ||||
|     "MULTISELECT": { | ||||
|       "ENTER_TO_SELECT": "Digite enter para selecionar", | ||||
|       "ENTER_TO_REMOVE": "Digite enter para remover", | ||||
|       "NO_OPTIONS": "List is empty", | ||||
|       "NO_OPTIONS": "Lista vazia", | ||||
|       "SELECT_ONE": "Selecione um", | ||||
|       "SELECT": "Selecionar" | ||||
|     } | ||||
|   | ||||
| @@ -160,8 +160,8 @@ | ||||
|         }, | ||||
|         "SEND_CNAME_INSTRUCTIONS": { | ||||
|           "API": { | ||||
|             "SUCCESS_MESSAGE": "CNAME instructions sent successfully", | ||||
|             "ERROR_MESSAGE": "Error while sending CNAME instructions" | ||||
|             "SUCCESS_MESSAGE": "Instruções do CNAME enviadas com sucesso", | ||||
|             "ERROR_MESSAGE": "Erro ao enviar as instruções CNAME" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
| @@ -732,7 +732,7 @@ | ||||
|         "HOME_PAGE_LINK": { | ||||
|           "LABEL": "Link da Página Inicial", | ||||
|           "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": { | ||||
|           "LABEL": "Slug", | ||||
| @@ -753,14 +753,14 @@ | ||||
|           "HEADER": "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.", | ||||
|           "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", | ||||
|           "EDIT_BUTTON": "Alterar", | ||||
|           "ADD_BUTTON": "Adicionar domínio personalizado", | ||||
|           "STATUS": { | ||||
|             "LIVE": "Em tempo real", | ||||
|             "PENDING": "Awaiting verification", | ||||
|             "ERROR": "Verification failed" | ||||
|             "PENDING": "Aguardando verificação", | ||||
|             "ERROR": "Verificação falhou" | ||||
|           }, | ||||
|           "DIALOG": { | ||||
|             "ADD_HEADER": "Adicionar domínio personalizado", | ||||
| @@ -770,17 +770,17 @@ | ||||
|             "LABEL": "Domínio personalizado", | ||||
|             "PLACEHOLDER": "Domínio personalizado do portal", | ||||
|             "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": { | ||||
|             "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", | ||||
|             "COPY": "Successfully copied CNAME", | ||||
|             "COPY": "CNAME copiado com sucesso", | ||||
|             "SEND_INSTRUCTIONS": { | ||||
|               "HEADER": "Send instructions", | ||||
|               "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.", | ||||
|               "PLACEHOLDER": "Enter their email", | ||||
|               "ERROR": "Enter a valid email address", | ||||
|               "HEADER": "Enviar instruções", | ||||
|               "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": "Insira o e-mail dele", | ||||
|               "ERROR": "Insira um endereço de e-mail válido", | ||||
|               "SEND_BUTTON": "Enviar" | ||||
|             } | ||||
|           } | ||||
|   | ||||
| @@ -74,21 +74,21 @@ | ||||
|       "DELETE_ALL_READ": "Todas as notificações lidas foram excluídas" | ||||
|     }, | ||||
|     "REAUTHORIZE": { | ||||
|       "TITLE": "Reauthorization Required", | ||||
|       "DESCRIPTION": "Your WhatsApp connection has expired. Please reconnect to continue receiving and sending messages.", | ||||
|       "BUTTON_TEXT": "Reconnect WhatsApp", | ||||
|       "LOADING_FACEBOOK": "Loading Facebook SDK...", | ||||
|       "SUCCESS": "WhatsApp reconnected successfully", | ||||
|       "ERROR": "Failed to reconnect WhatsApp. Please try again.", | ||||
|       "WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.", | ||||
|       "WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.", | ||||
|       "CONFIGURATION_ERROR": "Configuration error occurred during reauthorization.", | ||||
|       "FACEBOOK_LOAD_ERROR": "Failed to load Facebook SDK. Please try again.", | ||||
|       "TITLE": "Reautenticação necessária", | ||||
|       "DESCRIPTION": "Sua conexão com o WhatsApp expirou. Por favor, reconecte para continuar recebendo e enviando mensagens.", | ||||
|       "BUTTON_TEXT": "Reconectar WhatsApp", | ||||
|       "LOADING_FACEBOOK": "Carregando SDK do Facebook...", | ||||
|       "SUCCESS": "WhatsApp reconectado com sucesso", | ||||
|       "ERROR": "Falha ao reconectar o WhatsApp. Por favor, tente novamente.", | ||||
|       "WHATSAPP_APP_ID_MISSING": "WhatsApp App ID não está configurado. Por favor, contate seu administrador.", | ||||
|       "WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID não está configurado. Por favor, contate seu administrador.", | ||||
|       "CONFIGURATION_ERROR": "Ocorreu um erro de configuração ao reautenticar.", | ||||
|       "FACEBOOK_LOAD_ERROR": "Falha para carregar o SDK do Facebook. Por favor, tente novamente.", | ||||
|       "TROUBLESHOOTING": { | ||||
|         "TITLE": "Troubleshooting", | ||||
|         "POPUP_BLOCKED": "Ensure pop-ups are allowed for this site", | ||||
|         "COOKIES": "Third-party cookies must be enabled", | ||||
|         "ADMIN_ACCESS": "You need admin access to the WhatsApp Business Account" | ||||
|         "TITLE": "Solucionar problemas", | ||||
|         "POPUP_BLOCKED": "Certifique-se de que os pop-ups são permitidos para este site", | ||||
|         "COOKIES": "_Cookies_ de terceiros devem estar habilitados", | ||||
|         "ADMIN_ACCESS": "Você precisa de acesso de administrador na conta do WhatsApp Business" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -225,13 +225,13 @@ | ||||
|           "WHATSAPP_EMBEDDED": "WhatsApp Business", | ||||
|           "TWILIO": "Twilio", | ||||
|           "WHATSAPP_CLOUD": "Cloud do WhatsApp", | ||||
|           "WHATSAPP_CLOUD_DESC": "Quick setup through Meta", | ||||
|           "TWILIO_DESC": "Connect via Twilio credentials", | ||||
|           "WHATSAPP_CLOUD_DESC": "Configuração rápida via Meta", | ||||
|           "TWILIO_DESC": "Conectar através de credenciais Twilio", | ||||
|           "360_DIALOG": "360Dialog" | ||||
|         }, | ||||
|         "SELECT_PROVIDER": { | ||||
|           "TITLE": "Select your API provider", | ||||
|           "DESCRIPTION": "Choose your WhatsApp provider. You can connect directly through Meta which requires no setup, or connect through Twilio using your account credentials." | ||||
|           "TITLE": "Selecione seu provedor de API", | ||||
|           "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": { | ||||
|           "LABEL": "Nome da Caixa de Entrada", | ||||
| @@ -272,74 +272,74 @@ | ||||
|         }, | ||||
|         "SUBMIT_BUTTON": "Criar canal do WhatsApp", | ||||
|         "EMBEDDED_SIGNUP": { | ||||
|           "TITLE": "Quick Setup with 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.", | ||||
|           "TITLE": "Configuração rápida com Meta", | ||||
|           "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": { | ||||
|             "TITLE": "Benefits of Embedded Signup:", | ||||
|             "EASY_SETUP": "No manual configuration required", | ||||
|             "SECURE_AUTH": "Secure OAuth based authentication", | ||||
|             "AUTO_CONFIG": "Automatic webhook and phone number configuration" | ||||
|             "TITLE": "Benefícios da inscrição incorporada:", | ||||
|             "EASY_SETUP": "Nenhuma configuração manual é necessária", | ||||
|             "SECURE_AUTH": "Autenticação segura baseada em OAuth", | ||||
|             "AUTO_CONFIG": "Configuração automática de webhook e número de telefone" | ||||
|           }, | ||||
|           "LEARN_MORE": { | ||||
|             "TEXT": "To learn more about integrated signup, pricing, and limitations, visit", | ||||
|             "LINK_TEXT": "this link.", | ||||
|             "TEXT": "Para saber mais sobre inscrições integradas, preços e limitações visite", | ||||
|             "LINK_TEXT": "este link.", | ||||
|             "LINK_URL": "https://developers.facebook.com/docs/whatsapp/embedded-signup/custom-flows/onboarding-business-app-users#limitations" | ||||
|           }, | ||||
|           "SUBMIT_BUTTON": "Connect with WhatsApp Business", | ||||
|           "AUTH_PROCESSING": "Authenticating with Meta", | ||||
|           "WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...", | ||||
|           "PROCESSING": "Setting up your WhatsApp Business Account", | ||||
|           "LOADING_SDK": "Loading Facebook SDK...", | ||||
|           "CANCELLED": "WhatsApp Signup was cancelled", | ||||
|           "SUCCESS_TITLE": "WhatsApp Business Account Connected!", | ||||
|           "WAITING_FOR_AUTH": "Waiting for authentication...", | ||||
|           "INVALID_BUSINESS_DATA": "Invalid business data received from Facebook. Please try again.", | ||||
|           "SIGNUP_ERROR": "Signup error occurred", | ||||
|           "AUTH_NOT_COMPLETED": "Authentication not completed. Please restart the process.", | ||||
|           "SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured" | ||||
|           "SUBMIT_BUTTON": "Conecte-se com WhatsApp Business", | ||||
|           "AUTH_PROCESSING": "Autenticando com Meta", | ||||
|           "WAITING_FOR_BUSINESS_INFO": "Por favor, complete a configuração do negócio na janela da Meta...", | ||||
|           "PROCESSING": "Configurando sua conta do WhatsApp Business", | ||||
|           "LOADING_SDK": "Carregando SDK do Facebook...", | ||||
|           "CANCELLED": "A inscrição no WhatsApp foi cancelada", | ||||
|           "SUCCESS_TITLE": "Conta do WhatsApp Business conectada!", | ||||
|           "WAITING_FOR_AUTH": "Aguardando autenticação...", | ||||
|           "INVALID_BUSINESS_DATA": "Dados de negócio inválidos recebidos do Facebook. Por favor, tente novamente.", | ||||
|           "SIGNUP_ERROR": "Ocorreu um erro no cadastro", | ||||
|           "AUTH_NOT_COMPLETED": "Autenticação não concluída. Por favor, reinicie o processo.", | ||||
|           "SUCCESS_FALLBACK": "A conta do WhatsApp Business foi configurada com sucesso" | ||||
|         }, | ||||
|         "API": { | ||||
|           "ERROR_MESSAGE": "Não foi possível salvar o canal do WhatsApp" | ||||
|         } | ||||
|       }, | ||||
|       "VOICE": { | ||||
|         "TITLE": "Voice Channel", | ||||
|         "DESC": "Integrate Twilio Voice and start supporting your customers via phone calls.", | ||||
|         "TITLE": "Canal de Voz", | ||||
|         "DESC": "Integre o Twilio Voice e comece a oferecer suporte a seus clientes através de chamadas telefônicas.", | ||||
|         "PHONE_NUMBER": { | ||||
|           "LABEL": "Número de Telefone", | ||||
|           "PLACEHOLDER": "Enter your phone number (e.g. +1234567890)", | ||||
|           "ERROR": "Please provide a valid phone number in E.164 format (e.g. +1234567890)" | ||||
|           "PLACEHOLDER": "Digite seu número de telefone (por exemplo, +551234567890)", | ||||
|           "ERROR": "Por favor, forneça um número de telefone válido no formato E.164 (por exemplo, +551234567890)" | ||||
|         }, | ||||
|         "TWILIO": { | ||||
|           "ACCOUNT_SID": { | ||||
|             "LABEL": "SID da Conta", | ||||
|             "PLACEHOLDER": "Enter your Twilio Account SID", | ||||
|             "REQUIRED": "Account SID is required" | ||||
|             "PLACEHOLDER": "Insira o SID da sua Conta Twilio", | ||||
|             "REQUIRED": "O SID da conta é necessário" | ||||
|           }, | ||||
|           "AUTH_TOKEN": { | ||||
|             "LABEL": "Token de autenticação", | ||||
|             "PLACEHOLDER": "Enter your Twilio Auth Token", | ||||
|             "REQUIRED": "Auth Token is required" | ||||
|             "PLACEHOLDER": "Por favor, digite seu Token de Autenticação do Twilio", | ||||
|             "REQUIRED": "Um Token de autenticação é necessário" | ||||
|           }, | ||||
|           "API_KEY_SID": { | ||||
|             "LABEL": "Chave da API SID", | ||||
|             "PLACEHOLDER": "Enter your Twilio API Key SID", | ||||
|             "REQUIRED": "API Key SID is required" | ||||
|             "PLACEHOLDER": "Insira sua chave de API do Twilio SID", | ||||
|             "REQUIRED": "API Key SID é obrigatório" | ||||
|           }, | ||||
|           "API_KEY_SECRET": { | ||||
|             "LABEL": "Segredo da Chave API", | ||||
|             "PLACEHOLDER": "Enter your Twilio API Key Secret", | ||||
|             "REQUIRED": "API Key Secret is required" | ||||
|             "PLACEHOLDER": "Digite o segredo da sua chave de API do Twilio", | ||||
|             "REQUIRED": "Segredo da chave da API é obrigatório" | ||||
|           }, | ||||
|           "TWIML_APP_SID": { | ||||
|             "LABEL": "TwiML App SID", | ||||
|             "PLACEHOLDER": "Enter your Twilio TwiML App SID (starts with AP)", | ||||
|             "REQUIRED": "TwiML App SID is required" | ||||
|             "PLACEHOLDER": "Insira seu Twilio TwiML App SID (começa com AP)", | ||||
|             "REQUIRED": "TwiML App SID é obrigatório" | ||||
|           } | ||||
|         }, | ||||
|         "SUBMIT_BUTTON": "Create Voice Channel", | ||||
|         "SUBMIT_BUTTON": "Criar Canal de Voz", | ||||
|         "API": { | ||||
|           "ERROR_MESSAGE": "We were not able to create the voice channel" | ||||
|           "ERROR_MESSAGE": "Não conseguimos criar o canal de voz" | ||||
|         } | ||||
|       }, | ||||
|       "API_CHANNEL": { | ||||
| @@ -603,27 +603,27 @@ | ||||
|       "WHATSAPP_SECTION_UPDATE_TITLE": "Atualizar Chave de API", | ||||
|       "WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Digite a nova chave de API aqui", | ||||
|       "WHATSAPP_SECTION_UPDATE_BUTTON": "Atualizar", | ||||
|       "WHATSAPP_EMBEDDED_SIGNUP_TITLE": "WhatsApp Embedded Signup", | ||||
|       "WHATSAPP_EMBEDDED_SIGNUP_SUBHEADER": "This inbox is connected through WhatsApp embedded signup.", | ||||
|       "WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION": "You can reconfigure this inbox to update your WhatsApp Business settings.", | ||||
|       "WHATSAPP_RECONFIGURE_BUTTON": "Reconfigure", | ||||
|       "WHATSAPP_CONNECT_TITLE": "Connect to WhatsApp Business", | ||||
|       "WHATSAPP_CONNECT_SUBHEADER": "Upgrade to WhatsApp embedded signup for easier management.", | ||||
|       "WHATSAPP_CONNECT_DESCRIPTION": "Connect this inbox to WhatsApp Business for enhanced features and easier management.", | ||||
|       "WHATSAPP_EMBEDDED_SIGNUP_TITLE": "Inscrição incorporada do WhatsApp", | ||||
|       "WHATSAPP_EMBEDDED_SIGNUP_SUBHEADER": "Esta caixa de entrada está conectada através da inscrição incorporada do WhatsApp.", | ||||
|       "WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION": "Você pode reconfigurar esta caixa de entrada para atualizar suas configurações do WhatsApp Business.", | ||||
|       "WHATSAPP_RECONFIGURE_BUTTON": "Reconfigurar", | ||||
|       "WHATSAPP_CONNECT_TITLE": "Conectar ao WhatsApp Business", | ||||
|       "WHATSAPP_CONNECT_SUBHEADER": ".", | ||||
|       "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_SUCCESS": "Successfully connected to WhatsApp Business!", | ||||
|       "WHATSAPP_CONNECT_ERROR": "Failed to connect to WhatsApp Business. Please try again.", | ||||
|       "WHATSAPP_RECONFIGURE_SUCCESS": "Successfully reconfigured WhatsApp Business!", | ||||
|       "WHATSAPP_RECONFIGURE_ERROR": "Failed to reconfigure WhatsApp Business. Please try again.", | ||||
|       "WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.", | ||||
|       "WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.", | ||||
|       "WHATSAPP_LOGIN_CANCELLED": "WhatsApp login was cancelled. Please try again.", | ||||
|       "WHATSAPP_CONNECT_SUCCESS": "Conectado com sucesso ao WhatsApp Business!", | ||||
|       "WHATSAPP_CONNECT_ERROR": "Não foi possível reconfigurar o WhatsApp Business. Tente novamente.", | ||||
|       "WHATSAPP_RECONFIGURE_SUCCESS": "WhatsApp Business reconfigurado com sucesso!", | ||||
|       "WHATSAPP_RECONFIGURE_ERROR": "Não foi possível reconfigurar o WhatsApp Business. Tente novamente.", | ||||
|       "WHATSAPP_APP_ID_MISSING": "O ID do WhatsApp não está configurado. Por favor, contate o administrador.", | ||||
|       "WHATSAPP_CONFIG_ID_MISSING": "O ID de Configuração do WhatsApp não está configurado. Por favor, contate o administrador.", | ||||
|       "WHATSAPP_LOGIN_CANCELLED": "O login do WhatsApp foi cancelado. Por favor, tente novamente.", | ||||
|       "WHATSAPP_WEBHOOK_TITLE": "Token de verificação Webhook", | ||||
|       "WHATSAPP_WEBHOOK_SUBHEADER": "Este token é usado para verificar a autenticidade do webhook endpoint.", | ||||
|       "WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates", | ||||
|       "WHATSAPP_TEMPLATES_SYNC_SUBHEADER": "Manually sync message templates from WhatsApp to update your available templates.", | ||||
|       "WHATSAPP_TEMPLATES_SYNC_BUTTON": "Sync Templates", | ||||
|       "WHATSAPP_TEMPLATES_SYNC_SUCCESS": "Templates sync initiated successfully. It may take a couple of minutes to update.", | ||||
|       "WHATSAPP_TEMPLATES_SYNC_TITLE": "Sincronizar Modelos", | ||||
|       "WHATSAPP_TEMPLATES_SYNC_SUBHEADER": "Sincronize manualmente os modelos de mensagens do WhatsApp para atualizar seus modelos disponíveis.", | ||||
|       "WHATSAPP_TEMPLATES_SYNC_BUTTON": "Sincronizar Modelos", | ||||
|       "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" | ||||
|     }, | ||||
|     "HELP_CENTER": { | ||||
| @@ -883,7 +883,7 @@ | ||||
|       "LINE": "Line", | ||||
|       "API": "Canal da API", | ||||
|       "INSTAGRAM": "Instagram", | ||||
|       "VOICE": "Voice" | ||||
|       "VOICE": "Voz" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -334,8 +334,8 @@ | ||||
|     }, | ||||
|     "NOTION": { | ||||
|       "DELETE": { | ||||
|         "TITLE": "Are you sure you want to delete the Notion integration?", | ||||
|         "MESSAGE": "Deleting this integration will remove access to your Notion workspace and stop all related functionality.", | ||||
|         "TITLE": "Você tem certeza que deseja excluir a integração com Notion?", | ||||
|         "MESSAGE": "Excluir essa integração removerá o acesso ao seu espaço de trabalho Notion e encerrará todas as funcionalidades relacionadas.", | ||||
|         "CONFIRM": "Sim, excluir", | ||||
|         "CANCEL": "Cancelar" | ||||
|       } | ||||
| @@ -473,7 +473,7 @@ | ||||
|           "TITLE": "Funcionalidades", | ||||
|           "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_CITATIONS": "Include source citations in responses" | ||||
|           "ALLOW_CITATIONS": "Incluir fonte de citações nas respostas" | ||||
|         } | ||||
|       }, | ||||
|       "EDIT": { | ||||
| @@ -487,28 +487,28 @@ | ||||
|           "ASSISTANT": "Assistente" | ||||
|         }, | ||||
|         "BASIC_SETTINGS": { | ||||
|           "TITLE": "Basic settings", | ||||
|           "DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human." | ||||
|           "TITLE": "Configurações básicas", | ||||
|           "DESCRIPTION": "Personalize o que o assistente diz quando termina uma conversa ou transfere para um humano." | ||||
|         }, | ||||
|         "SYSTEM_SETTINGS": { | ||||
|           "TITLE": "System settings", | ||||
|           "DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human." | ||||
|           "TITLE": "Configurações do sistema", | ||||
|           "DESCRIPTION": "Personalize o que o assistente diz quando termina uma conversa ou transfere para um humano." | ||||
|         }, | ||||
|         "CONTROL_ITEMS": { | ||||
|           "TITLE": "The Fun Stuff", | ||||
|           "DESCRIPTION": "Add more control to the assistant. (a bit more visual like a story : Query guardrail → scenarios → output) Nudges user to actually utilise these.", | ||||
|           "TITLE": "As Coisas Divertidas", | ||||
|           "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": { | ||||
|             "GUARDRAILS": { | ||||
|               "TITLE": "Guardrails", | ||||
|               "DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic." | ||||
|               "TITLE": "Proteções", | ||||
|               "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": { | ||||
|               "TITLE": "Scenarios", | ||||
|               "DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”" | ||||
|               "TITLE": "Cenários", | ||||
|               "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": { | ||||
|               "TITLE": "Response guidelines", | ||||
|               "DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?" | ||||
|               "TITLE": "Diretrizes de resposta", | ||||
|               "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": { | ||||
|         "TITLE": "Guardrails", | ||||
|         "DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.", | ||||
|         "TITLE": "Proteções", | ||||
|         "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": { | ||||
|           "TITLE": "Guardrails" | ||||
|           "TITLE": "Proteções" | ||||
|         }, | ||||
|         "BULK_ACTION": { | ||||
|           "SELECTED": "{count} item selected | {count} items selected", | ||||
|           "SELECTED": "{count} item selecionado | {count} itens selecionados", | ||||
|           "SELECT_ALL": "Selecionar todos ({count})", | ||||
|           "UNSELECT_ALL": "Desmarcar todos ({count})", | ||||
|           "BULK_DELETE_BUTTON": "Excluir" | ||||
|         }, | ||||
|         "ADD": { | ||||
|           "SUGGESTED": { | ||||
|             "TITLE": "Example guardrails", | ||||
|             "ADD": "Add all", | ||||
|             "ADD_SINGLE": "Add this", | ||||
|             "SAVE": "Add and save (↵)", | ||||
|             "PLACEHOLDER": "Type in another guardrail..." | ||||
|             "TITLE": "Exemplos de proteções", | ||||
|             "ADD": "Adicionar todos", | ||||
|             "ADD_SINGLE": "Adicionar este", | ||||
|             "SAVE": "Adicionar e salvar (↵)", | ||||
|             "PLACEHOLDER": "Escreva outra proteção" | ||||
|           }, | ||||
|           "NEW": { | ||||
|             "TITLE": "Add a guardrail", | ||||
|             "TITLE": "Adicionar proteção", | ||||
|             "CREATE": "Criar", | ||||
|             "CANCEL": "Cancelar", | ||||
|             "PLACEHOLDER": "Type in another guardrail...", | ||||
|             "TEST_ALL": "Test all" | ||||
|             "PLACEHOLDER": "Escreva outra proteção", | ||||
|             "TEST_ALL": "Testar tudo" | ||||
|           } | ||||
|         }, | ||||
|         "LIST": { | ||||
|           "SEARCH_PLACEHOLDER": "Pesquisar..." | ||||
|         }, | ||||
|         "EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.", | ||||
|         "SEARCH_EMPTY_MESSAGE": "No guardrails found for this search.", | ||||
|         "EMPTY_MESSAGE": "Nenhuma proteção encontrada. Crie uma ou adicione exemplos para começar.", | ||||
|         "SEARCH_EMPTY_MESSAGE": "Nenhuma proteção encontrada para essa pesquisa.", | ||||
|         "API": { | ||||
|           "ADD": { | ||||
|             "SUCCESS": "Guardrails added successfully", | ||||
|             "ERROR": "There was an error adding guardrails, please try again." | ||||
|             "SUCCESS": "Proteções adicionadas com sucesso", | ||||
|             "ERROR": "Ocorreu um erro ao adicionar as proteções. Por favor, tente novamente." | ||||
|           }, | ||||
|           "UPDATE": { | ||||
|             "SUCCESS": "Guardrails updated successfully", | ||||
|             "ERROR": "There was an error updating guardrails, please try again." | ||||
|             "SUCCESS": "Proteções atualizados com sucesso", | ||||
|             "ERROR": "Ocorreu um erro ao atualizar as proteções. Por favor, tente novamente." | ||||
|           }, | ||||
|           "DELETE": { | ||||
|             "SUCCESS": "Guardrails deleted successfully", | ||||
|             "ERROR": "There was an error deleting guardrails, please try again." | ||||
|             "SUCCESS": "Proteções removidas com sucesso", | ||||
|             "ERROR": "Ocorreu um erro ao excluir as proteções, por favor, tente novamente." | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "RESPONSE_GUIDELINES": { | ||||
|         "TITLE": "Response Guidelines", | ||||
|         "DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?", | ||||
|         "TITLE": "Diretrizes de Resposta", | ||||
|         "DESCRIPTION": "O jeito e a estrutura das respostas do seu assistente — tranquilo e amigável? Curto e ágil? Detalhado e formal?", | ||||
|         "BREADCRUMB": { | ||||
|           "TITLE": "Response Guidelines" | ||||
|           "TITLE": "Diretrizes de Resposta" | ||||
|         }, | ||||
|         "BULK_ACTION": { | ||||
|           "SELECTED": "{count} item selected | {count} items selected", | ||||
|           "SELECTED": "{count} item selecionado | {count} itens selecionados", | ||||
|           "SELECT_ALL": "Selecionar todos ({count})", | ||||
|           "UNSELECT_ALL": "Desmarcar todos ({count})", | ||||
|           "BULK_DELETE_BUTTON": "Excluir" | ||||
|         }, | ||||
|         "ADD": { | ||||
|           "SUGGESTED": { | ||||
|             "TITLE": "Example response guidelines", | ||||
|             "ADD": "Add all", | ||||
|             "ADD_SINGLE": "Add this", | ||||
|             "SAVE": "Add and save (↵)", | ||||
|             "PLACEHOLDER": "Type in another response guideline..." | ||||
|             "TITLE": "Exemplos de diretrizes de resposta", | ||||
|             "ADD": "Adicionar todos", | ||||
|             "ADD_SINGLE": "Adicionar este", | ||||
|             "SAVE": "Adicionar e salvar (↵)", | ||||
|             "PLACEHOLDER": "Escreva uma outra diretriz de resposta..." | ||||
|           }, | ||||
|           "NEW": { | ||||
|             "TITLE": "Add a response guideline", | ||||
|             "TITLE": "Adicione uma diretriz de resposta", | ||||
|             "CREATE": "Criar", | ||||
|             "CANCEL": "Cancelar", | ||||
|             "PLACEHOLDER": "Type in another response guideline...", | ||||
|             "TEST_ALL": "Test all" | ||||
|             "PLACEHOLDER": "Escreva uma outra diretriz de resposta...", | ||||
|             "TEST_ALL": "Testar tudo" | ||||
|           } | ||||
|         }, | ||||
|         "LIST": { | ||||
|           "SEARCH_PLACEHOLDER": "Pesquisar..." | ||||
|         }, | ||||
|         "EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.", | ||||
|         "SEARCH_EMPTY_MESSAGE": "No response guidelines found for this search.", | ||||
|         "EMPTY_MESSAGE": "Nenhuma diretriz de resposta encontrada. Crie uma ou adicione exemplos para começar.", | ||||
|         "SEARCH_EMPTY_MESSAGE": "Nenhuma diretriz de resposta encotrada para essa pesquisa.", | ||||
|         "API": { | ||||
|           "ADD": { | ||||
|             "SUCCESS": "Response Guidelines added successfully", | ||||
|             "ERROR": "There was an error adding response guidelines, please try again." | ||||
|             "SUCCESS": "Diretrizes de resposta adicionadas com sucesso", | ||||
|             "ERROR": "Houve um erro ao adicionar diretrizes de resposta, por favor, tente novamente." | ||||
|           }, | ||||
|           "UPDATE": { | ||||
|             "SUCCESS": "Response Guidelines updated successfully", | ||||
|             "ERROR": "There was an error updating response guidelines, please try again." | ||||
|             "SUCCESS": "Diretrizes de Resposta atualizadas com sucesso", | ||||
|             "ERROR": "Houve um erro ao atualizar as diretrizes de resposta, por favor, tente novamente." | ||||
|           }, | ||||
|           "DELETE": { | ||||
|             "SUCCESS": "Response Guidelines deleted successfully", | ||||
|             "ERROR": "There was an error deleting response guidelines, please try again." | ||||
|             "SUCCESS": "Diretrizes de resposta removidas com sucesso", | ||||
|             "ERROR": "Houve um erro ao excluir as diretrizes de resposta, por favor, tente novamente." | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "SCENARIOS": { | ||||
|         "TITLE": "Scenarios", | ||||
|         "DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”", | ||||
|         "TITLE": "Cenários", | ||||
|         "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": { | ||||
|           "TITLE": "Scenarios" | ||||
|           "TITLE": "Cenários" | ||||
|         }, | ||||
|         "BULK_ACTION": { | ||||
|           "SELECTED": "{count} item selected | {count} items selected", | ||||
|           "SELECTED": "{count} item selecionado | {count} itens selecionados", | ||||
|           "SELECT_ALL": "Selecionar todos ({count})", | ||||
|           "UNSELECT_ALL": "Desmarcar todos ({count})", | ||||
|           "BULK_DELETE_BUTTON": "Excluir" | ||||
|         }, | ||||
|         "ADD": { | ||||
|           "SUGGESTED": { | ||||
|             "TITLE": "Example scenarios", | ||||
|             "ADD": "Add all", | ||||
|             "ADD_SINGLE": "Add this", | ||||
|             "TOOLS_USED": "Tools used :" | ||||
|             "TITLE": "Exemplos de cenários", | ||||
|             "ADD": "Adicionar todos", | ||||
|             "ADD_SINGLE": "Adicionar este", | ||||
|             "TOOLS_USED": "Ferramentas usadas :" | ||||
|           }, | ||||
|           "NEW": { | ||||
|             "CREATE": "Add a scenario", | ||||
|             "TITLE": "Create a scenario", | ||||
|             "CREATE": "Adicionar um cenário", | ||||
|             "TITLE": "Criar um cenário", | ||||
|             "FORM": { | ||||
|               "TITLE": { | ||||
|                 "LABEL": "Título", | ||||
|                 "PLACEHOLDER": "Enter a name for the scenario", | ||||
|                 "ERROR": "Scenario name is required" | ||||
|                 "PLACEHOLDER": "Digite um nome para o cenário", | ||||
|                 "ERROR": "O nome do cenário é obrigatório" | ||||
|               }, | ||||
|               "DESCRIPTION": { | ||||
|                 "LABEL": "Descrição", | ||||
|                 "PLACEHOLDER": "Describe how and where this scenario will be used", | ||||
|                 "ERROR": "Scenario description is required" | ||||
|                 "PLACEHOLDER": "Descreva como e onde este cenário será utilizado", | ||||
|                 "ERROR": "Descrição do cenário é obrigatória" | ||||
|               }, | ||||
|               "INSTRUCTION": { | ||||
|                 "LABEL": "How to handle", | ||||
|                 "PLACEHOLDER": "Describe how and where this scenario will be handled", | ||||
|                 "ERROR": "Scenario content is required" | ||||
|                 "LABEL": "Como lidar", | ||||
|                 "PLACEHOLDER": "Descreva como e onde este cenário será utilizado", | ||||
|                 "ERROR": "Conteúdo do cenário é obrigatório" | ||||
|               }, | ||||
|               "CREATE": "Criar", | ||||
|               "CANCEL": "Cancelar" | ||||
| @@ -667,25 +667,25 @@ | ||||
|         }, | ||||
|         "UPDATE": { | ||||
|           "CANCEL": "Cancelar", | ||||
|           "UPDATE": "Update changes" | ||||
|           "UPDATE": "Atualizar alterações" | ||||
|         }, | ||||
|         "LIST": { | ||||
|           "SEARCH_PLACEHOLDER": "Pesquisar..." | ||||
|         }, | ||||
|         "EMPTY_MESSAGE": "No scenarios found. Create or add examples to begin.", | ||||
|         "SEARCH_EMPTY_MESSAGE": "No scenarios found for this search.", | ||||
|         "EMPTY_MESSAGE": "Nenhum cenário encontrado. Crie ou adicione exemplos para começar.", | ||||
|         "SEARCH_EMPTY_MESSAGE": "Nenhum cenário encontrado para esta pesquisa.", | ||||
|         "API": { | ||||
|           "ADD": { | ||||
|             "SUCCESS": "Scenarios added successfully", | ||||
|             "ERROR": "There was an error adding scenarios, please try again." | ||||
|             "SUCCESS": "Cenários adicionados com sucesso", | ||||
|             "ERROR": "Ocorreu um erro ao adicionar cenários, por favor tente novamente." | ||||
|           }, | ||||
|           "UPDATE": { | ||||
|             "SUCCESS": "Scenarios updated successfully", | ||||
|             "ERROR": "There was an error updating scenarios, please try again." | ||||
|             "SUCCESS": "Cenários atualizados com sucesso", | ||||
|             "ERROR": "Ocorreu um erro ao atualizar cenários, por favor tente novamente." | ||||
|           }, | ||||
|           "DELETE": { | ||||
|             "SUCCESS": "Scenarios deleted successfully", | ||||
|             "ERROR": "There was an error deleting scenarios, please try again." | ||||
|             "SUCCESS": "Cenários excluídos com sucesso", | ||||
|             "ERROR": "Ocorreu um erro ao excluir os cenários, por favor tente novamente." | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|   | ||||
| @@ -3,22 +3,22 @@ | ||||
|     "MODAL": { | ||||
|       "TITLE": "Templates do Whatsapp", | ||||
|       "SUBTITLE": "Selecione o template do whatsapp que você deseja enviar", | ||||
|       "TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}" | ||||
|       "TEMPLATE_SELECTED_SUBTITLE": "Configurar modelo: {templateName}" | ||||
|     }, | ||||
|     "PICKER": { | ||||
|       "SEARCH_PLACEHOLDER": "Pesquisar modelos", | ||||
|       "NO_TEMPLATES_FOUND": "Não há templates encontrados para", | ||||
|       "HEADER": "Header", | ||||
|       "BODY": "Body", | ||||
|       "FOOTER": "Footer", | ||||
|       "BUTTONS": "Buttons", | ||||
|       "HEADER": "Cabeçalho", | ||||
|       "BODY": "Corpo", | ||||
|       "FOOTER": "Rodapé", | ||||
|       "BUTTONS": "Botões", | ||||
|       "CATEGORY": "Categoria", | ||||
|       "MEDIA_CONTENT": "Media Content", | ||||
|       "MEDIA_CONTENT_FALLBACK": "media content", | ||||
|       "NO_TEMPLATES_AVAILABLE": "No WhatsApp templates available. Click refresh to sync templates from WhatsApp.", | ||||
|       "REFRESH_BUTTON": "Refresh templates", | ||||
|       "REFRESH_SUCCESS": "Templates refresh initiated. It may take a couple of minutes to update.", | ||||
|       "REFRESH_ERROR": "Failed to refresh templates. Please try again.", | ||||
|       "MEDIA_CONTENT": "Conteúdo de Mídia", | ||||
|       "MEDIA_CONTENT_FALLBACK": "conteúdo de mídia", | ||||
|       "NO_TEMPLATES_AVAILABLE": "Não há modelos disponíveis do WhatsApp. Clique em atualizar para sincronizar os modelos do WhatsApp.", | ||||
|       "REFRESH_BUTTON": "Atualizar modelos", | ||||
|       "REFRESH_SUCCESS": "Atualização de modelos iniciada. Pode levar alguns minutos para atualizar.", | ||||
|       "REFRESH_ERROR": "Falha ao atualizar os modelos. Por favor, tente novamente.", | ||||
|       "LABELS": { | ||||
|         "LANGUAGE": "Idioma", | ||||
|         "TEMPLATE_BODY": "Conteúdo do Template", | ||||
| @@ -33,14 +33,14 @@ | ||||
|       "GO_BACK_LABEL": "Voltar", | ||||
|       "SEND_MESSAGE_LABEL": "Enviar Mensagem", | ||||
|       "FORM_ERROR_MESSAGE": "Por favor, preencha todas as variáveis antes de enviar", | ||||
|       "MEDIA_HEADER_LABEL": "{type} Header", | ||||
|       "OTP_CODE": "Enter 4-8 digit OTP", | ||||
|       "EXPIRY_MINUTES": "Enter expiry minutes", | ||||
|       "BUTTON_PARAMETERS": "Button Parameters", | ||||
|       "BUTTON_LABEL": "Button {index}", | ||||
|       "COUPON_CODE": "Enter coupon code (max 15 chars)", | ||||
|       "MEDIA_URL_LABEL": "Enter {type} URL", | ||||
|       "BUTTON_PARAMETER": "Enter button parameter" | ||||
|       "MEDIA_HEADER_LABEL": "Cabeçalho {type}", | ||||
|       "OTP_CODE": "Digite OTP de 4 a 8 dígitos", | ||||
|       "EXPIRY_MINUTES": "Digite os minutos de expiração", | ||||
|       "BUTTON_PARAMETERS": "Parâmetros do botão", | ||||
|       "BUTTON_LABEL": "Botão {index}", | ||||
|       "COUPON_CODE": "Digite o código do cupom (máx. 15 caracteres)", | ||||
|       "MEDIA_URL_LABEL": "Digite a URL {type}", | ||||
|       "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 { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||
| import NextButton from 'dashboard/components-next/button/Button.vue'; | ||||
| import VoiceCallButton from 'dashboard/components-next/Contacts/VoiceCallButton.vue'; | ||||
|  | ||||
| import { | ||||
|   isAConversationRoute, | ||||
| @@ -28,6 +29,7 @@ export default { | ||||
|     ComposeConversation, | ||||
|     SocialIcons, | ||||
|     ContactMergeModal, | ||||
|     VoiceCallButton, | ||||
|   }, | ||||
|   props: { | ||||
|     contact: { | ||||
| @@ -278,6 +280,14 @@ export default { | ||||
|             /> | ||||
|           </template> | ||||
|         </ComposeConversation> | ||||
|         <VoiceCallButton | ||||
|           :phone="contact.phone_number" | ||||
|           icon="i-ri-phone-fill" | ||||
|           size="sm" | ||||
|           :tooltip-label="$t('CONTACT_PANEL.CALL')" | ||||
|           slate | ||||
|           faded | ||||
|         /> | ||||
|         <NextButton | ||||
|           v-tooltip.top-end="$t('EDIT_CONTACT.BUTTON_LABEL')" | ||||
|           icon="i-ph-pencil-simple" | ||||
|   | ||||
| @@ -55,15 +55,8 @@ export default { | ||||
|     emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.toggleReplyTo); | ||||
|   }, | ||||
|   methods: { | ||||
|     ...mapActions('conversation', [ | ||||
|       'sendMessage', | ||||
|       'sendAttachment', | ||||
|       'clearConversations', | ||||
|     ]), | ||||
|     ...mapActions('conversationAttributes', [ | ||||
|       'getAttributes', | ||||
|       'clearConversationAttributes', | ||||
|     ]), | ||||
|     ...mapActions('conversation', ['sendMessage', 'sendAttachment']), | ||||
|     ...mapActions('conversationAttributes', ['getAttributes']), | ||||
|     async handleSendMessage(content) { | ||||
|       await this.sendMessage({ | ||||
|         content, | ||||
| @@ -84,8 +77,6 @@ export default { | ||||
|       this.inReplyTo = null; | ||||
|     }, | ||||
|     startNewConversation() { | ||||
|       this.clearConversations(); | ||||
|       this.clearConversationAttributes(); | ||||
|       this.replaceRoute('prechat-form'); | ||||
|       IFrameHelper.sendMessage({ | ||||
|         event: 'onEvent', | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
|   }, | ||||
|   "THUMBNAIL": { | ||||
|     "AUTHOR": { | ||||
|       "NOT_AVAILABLE": "Not available" | ||||
|       "NOT_AVAILABLE": "Indisponível" | ||||
|     } | ||||
|   }, | ||||
|   "TEAM_AVAILABILITY": { | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| <script> | ||||
| import { mapActions } from 'vuex'; | ||||
| import PreChatForm from '../components/PreChat/Form.vue'; | ||||
| import configMixin from '../mixins/configMixin'; | ||||
| import routerMixin from '../mixins/routerMixin'; | ||||
| @@ -19,6 +20,8 @@ export default { | ||||
|     emitter.off(ON_CONVERSATION_CREATED, this.handleConversationCreated); | ||||
|   }, | ||||
|   methods: { | ||||
|     ...mapActions('conversation', ['clearConversations']), | ||||
|     ...mapActions('conversationAttributes', ['clearConversationAttributes']), | ||||
|     handleConversationCreated() { | ||||
|       // Redirect to messages page after conversation is created | ||||
|       this.replaceRoute('messages'); | ||||
| @@ -48,6 +51,8 @@ export default { | ||||
|           }, | ||||
|         }); | ||||
|       } else { | ||||
|         this.clearConversations(); | ||||
|         this.clearConversationAttributes(); | ||||
|         this.$store.dispatch('conversation/createConversation', { | ||||
|           fullName: fullName, | ||||
|           emailAddress: emailAddress, | ||||
|   | ||||
| @@ -61,6 +61,7 @@ class Account < ApplicationRecord | ||||
|   has_many :agent_bots, dependent: :destroy_async | ||||
|   has_many :api_channels, dependent: :destroy_async, class_name: '::Channel::Api' | ||||
|   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 :macros, 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 :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, through: :agent_bot_inbox | ||||
|   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 | ||||
|   version: '4.5.0' | ||||
|   version: '4.5.1' | ||||
|  | ||||
| development: | ||||
|   <<: *shared | ||||
|   | ||||
| @@ -191,3 +191,7 @@ | ||||
|   display_name: CRM V2 | ||||
|   enabled: false | ||||
|   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}' | ||||
|       invalid_params: 'Invalid, please check the signup paramters and try again' | ||||
|       failed: Signup failed | ||||
|     assignment_policy: | ||||
|       not_found: Assignment policy not found | ||||
|     data_import: | ||||
|       data_type: | ||||
|         invalid: Invalid data type | ||||
|   | ||||
| @@ -20,9 +20,9 @@ pt_BR: | ||||
|   hello: 'Olá, mundo' | ||||
|   inbox: | ||||
|     reauthorization: | ||||
|       success: 'Channel reauthorized successfully' | ||||
|       not_required: 'Reauthorization is not required for this inbox' | ||||
|       invalid_channel: 'Invalid channel type for reauthorization' | ||||
|       success: 'Canal reautenticado com sucesso' | ||||
|       not_required: 'Reautenticação não é necessária para esta caixa de entrada' | ||||
|       invalid_channel: 'Tipo de canal inválido para reautenticar' | ||||
|   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_failure: Uh ho! Não conseguimos encontrar nenhum usuário com o e-mail especificado. | ||||
| @@ -59,12 +59,12 @@ pt_BR: | ||||
|     slack: | ||||
|       invalid_channel_id: 'Canal de slack inválido. Por favor, tente novamente' | ||||
|     whatsapp: | ||||
|       token_exchange_failed: 'Failed to exchange code for access token. Please try again.' | ||||
|       invalid_token_permissions: 'The access token does not have the required permissions for WhatsApp.' | ||||
|       phone_info_fetch_failed: 'Failed to fetch phone number information. Please try again.' | ||||
|       token_exchange_failed: 'Falha ao trocar o código por um token de acesso. Por favor, tente novamente.' | ||||
|       invalid_token_permissions: 'O token de acesso não tem as permissões necessárias para o WhatsApp.' | ||||
|       phone_info_fetch_failed: 'Falha ao obter a informação do número de telefone. Por favor, tente novamente.' | ||||
|       reauthorization: | ||||
|         generic: 'Failed to reauthorize WhatsApp. Please try again.' | ||||
|         not_supported: 'Reauthorization is not supported for this type of WhatsApp channel.' | ||||
|         generic: 'Falha ao reautenticar o WhatsApp. Por favor, tente novamente.' | ||||
|         not_supported: 'Reautenticação não é suportado por este tipo de canal WhatsApp.' | ||||
|     inboxes: | ||||
|       imap: | ||||
|         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.' | ||||
|     notion: | ||||
|       name: 'Notion' | ||||
|       short_description: 'Integrate databases, documents and pages directly with Captain.' | ||||
|       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.' | ||||
|       short_description: 'Integre banco de dados, documentos e páginas diretamente com o Capitão.' | ||||
|       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: | ||||
|       name: 'Shopify' | ||||
|       short_description: 'Acessar detalhes do pedido e dados de clientes da sua loja Shopify.' | ||||
| @@ -359,9 +359,9 @@ pt_BR: | ||||
|   portals: | ||||
|     send_instructions: | ||||
|       email_required: 'E-mail é obrigatório' | ||||
|       invalid_email_format: 'Invalid email format' | ||||
|       custom_domain_not_configured: 'Custom domain is not configured' | ||||
|       instructions_sent_successfully: 'Instructions sent successfully' | ||||
|       subject: 'Finish setting up %{custom_domain}' | ||||
|       invalid_email_format: 'Formato inválido de e-mail' | ||||
|       custom_domain_not_configured: 'Domínio personalizado não está configurado' | ||||
|       instructions_sent_successfully: 'Instruções enviadas com sucesso' | ||||
|       subject: 'Termine de configurar %{custom_domain}' | ||||
|     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 | ||||
|  | ||||
|           # 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 | ||||
|             resource :authorization, only: [:create] | ||||
|           end | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| class AddFeatureCitationToAssistantConfig < ActiveRecord::Migration[7.1] | ||||
|   def up | ||||
|     return unless ChatwootApp.enterprise? | ||||
|  | ||||
|     Captain::Assistant.find_each do |assistant| | ||||
|       assistant.update!( | ||||
|         config: assistant.config.merge('feature_citation' => true) | ||||
| @@ -8,6 +10,8 @@ class AddFeatureCitationToAssistantConfig < ActiveRecord::Migration[7.1] | ||||
|   end | ||||
|  | ||||
|   def down | ||||
|     return unless ChatwootApp.enterprise? | ||||
|  | ||||
|     Captain::Assistant.find_each do |assistant| | ||||
|       config = assistant.config.dup | ||||
|       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", | ||||
|   "version": "4.5.0", | ||||
|   "version": "4.5.1", | ||||
|   "license": "MIT", | ||||
|   "scripts": { | ||||
|     "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