+
{{ currentUser.available_name }}
-
+
{{ currentUser.email }}
-
+
diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json
index 4f0a27cae..a5c9549f1 100644
--- a/app/javascript/dashboard/i18n/locale/en/contact.json
+++ b/app/javascript/dashboard/i18n/locale/en/contact.json
@@ -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"
diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json
index fde198c92..d547538db 100644
--- a/app/javascript/dashboard/i18n/locale/en/settings.json
+++ b/app/javascript/dashboard/i18n/locale/en/settings.json
@@ -226,6 +226,7 @@
"APPEARANCE": "Change appearance",
"SUPER_ADMIN_CONSOLE": "SuperAdmin console",
"DOCS": "Read documentation",
+ "CHANGELOG": "Changelog",
"LOGOUT": "Log out"
},
"APP_GLOBAL": {
diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/attributesMgmt.json b/app/javascript/dashboard/i18n/locale/pt_BR/attributesMgmt.json
index 9a2295a4f..5011e70b9 100644
--- a/app/javascript/dashboard/i18n/locale/pt_BR/attributesMgmt.json
+++ b/app/javascript/dashboard/i18n/locale/pt_BR/attributesMgmt.json
@@ -13,7 +13,7 @@
"TEXT": "Texto",
"NUMBER": "Número",
"LINK": "Link",
- "DATE": "Date",
+ "DATE": "Data",
"LIST": "Lista",
"CHECKBOX": "Checkbox"
},
diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/automation.json b/app/javascript/dashboard/i18n/locale/pt_BR/automation.json
index a3ecb50ac..294432a4c 100644
--- a/app/javascript/dashboard/i18n/locale/pt_BR/automation.json
+++ b/app/javascript/dashboard/i18n/locale/pt_BR/automation.json
@@ -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",
diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/campaign.json b/app/javascript/dashboard/i18n/locale/pt_BR/campaign.json
index dc914f1f1..c496f84e5 100644
--- a/app/javascript/dashboard/i18n/locale/pt_BR/campaign.json
+++ b/app/javascript/dashboard/i18n/locale/pt_BR/campaign.json
@@ -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."
}
}
diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/components.json b/app/javascript/dashboard/i18n/locale/pt_BR/components.json
index 999171733..748b9eab5 100644
--- a/app/javascript/dashboard/i18n/locale/pt_BR/components.json
+++ b/app/javascript/dashboard/i18n/locale/pt_BR/components.json
@@ -51,6 +51,6 @@
"PLACEHOLDER": "Insira a duração"
},
"CHANNEL_SELECTOR": {
- "COMING_SOON": "Coming Soon!"
+ "COMING_SOON": "Em breve!"
}
}
diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/conversation.json b/app/javascript/dashboard/i18n/locale/pt_BR/conversation.json
index 538d02ad7..6c681572d 100644
--- a/app/javascript/dashboard/i18n/locale/pt_BR/conversation.json
+++ b/app/javascript/dashboard/i18n/locale/pt_BR/conversation.json
@@ -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}\"",
diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/generalSettings.json b/app/javascript/dashboard/i18n/locale/pt_BR/generalSettings.json
index dfbee86f4..28bdffdd9 100644
--- a/app/javascript/dashboard/i18n/locale/pt_BR/generalSettings.json
+++ b/app/javascript/dashboard/i18n/locale/pt_BR/generalSettings.json
@@ -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"
}
diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/helpCenter.json b/app/javascript/dashboard/i18n/locale/pt_BR/helpCenter.json
index b438afa7d..a76147ca0 100644
--- a/app/javascript/dashboard/i18n/locale/pt_BR/helpCenter.json
+++ b/app/javascript/dashboard/i18n/locale/pt_BR/helpCenter.json
@@ -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"
}
}
diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/inbox.json b/app/javascript/dashboard/i18n/locale/pt_BR/inbox.json
index d3750f133..dc0ba7834 100644
--- a/app/javascript/dashboard/i18n/locale/pt_BR/inbox.json
+++ b/app/javascript/dashboard/i18n/locale/pt_BR/inbox.json
@@ -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"
}
}
}
diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json
index 3385491a1..adf445252 100644
--- a/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json
+++ b/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json
@@ -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"
}
}
}
diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/integrations.json b/app/javascript/dashboard/i18n/locale/pt_BR/integrations.json
index 67102dbb5..88624a8d0 100644
--- a/app/javascript/dashboard/i18n/locale/pt_BR/integrations.json
+++ b/app/javascript/dashboard/i18n/locale/pt_BR/integrations.json
@@ -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."
}
}
}
diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/whatsappTemplates.json b/app/javascript/dashboard/i18n/locale/pt_BR/whatsappTemplates.json
index 27accbaa8..de844053a 100644
--- a/app/javascript/dashboard/i18n/locale/pt_BR/whatsappTemplates.json
+++ b/app/javascript/dashboard/i18n/locale/pt_BR/whatsappTemplates.json
@@ -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"
}
}
}
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
index 327bc95ae..bfdb5d68a 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
@@ -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 {
/>
+
+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,
diff --git a/app/models/account.rb b/app/models/account.rb
index f8eb998f0..b84d7f526 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -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
diff --git a/app/models/assignment_policy.rb b/app/models/assignment_policy.rb
new file mode 100644
index 000000000..c01ab91c4
--- /dev/null
+++ b/app/models/assignment_policy.rb
@@ -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')
diff --git a/app/models/inbox.rb b/app/models/inbox.rb
index 1c898ba7f..27f096bfa 100644
--- a/app/models/inbox.rb
+++ b/app/models/inbox.rb
@@ -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
diff --git a/app/models/inbox_assignment_policy.rb b/app/models/inbox_assignment_policy.rb
new file mode 100644
index 000000000..c263ab40e
--- /dev/null
+++ b/app/models/inbox_assignment_policy.rb
@@ -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
diff --git a/app/policies/assignment_policy_policy.rb b/app/policies/assignment_policy_policy.rb
new file mode 100644
index 000000000..fcd0ee9bc
--- /dev/null
+++ b/app/policies/assignment_policy_policy.rb
@@ -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
diff --git a/app/views/api/v1/accounts/assignment_policies/_assignment_policy.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/_assignment_policy.json.jbuilder
new file mode 100644
index 000000000..b48307a94
--- /dev/null
+++ b/app/views/api/v1/accounts/assignment_policies/_assignment_policy.json.jbuilder
@@ -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
diff --git a/app/views/api/v1/accounts/assignment_policies/create.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/create.json.jbuilder
new file mode 100644
index 000000000..8fd9543c3
--- /dev/null
+++ b/app/views/api/v1/accounts/assignment_policies/create.json.jbuilder
@@ -0,0 +1 @@
+json.partial! 'assignment_policy', assignment_policy: @assignment_policy
diff --git a/app/views/api/v1/accounts/assignment_policies/inboxes/create.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/inboxes/create.json.jbuilder
new file mode 100644
index 000000000..c5aede050
--- /dev/null
+++ b/app/views/api/v1/accounts/assignment_policies/inboxes/create.json.jbuilder
@@ -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
diff --git a/app/views/api/v1/accounts/assignment_policies/inboxes/index.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/inboxes/index.json.jbuilder
new file mode 100644
index 000000000..5a22aa917
--- /dev/null
+++ b/app/views/api/v1/accounts/assignment_policies/inboxes/index.json.jbuilder
@@ -0,0 +1,3 @@
+json.inboxes @inboxes do |inbox|
+ json.partial! 'api/v1/models/inbox', formats: [:json], resource: inbox
+end
diff --git a/app/views/api/v1/accounts/assignment_policies/index.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/index.json.jbuilder
new file mode 100644
index 000000000..0be431f87
--- /dev/null
+++ b/app/views/api/v1/accounts/assignment_policies/index.json.jbuilder
@@ -0,0 +1,3 @@
+json.array! @assignment_policies do |assignment_policy|
+ json.partial! 'assignment_policy', assignment_policy: assignment_policy
+end
diff --git a/app/views/api/v1/accounts/assignment_policies/show.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/show.json.jbuilder
new file mode 100644
index 000000000..8fd9543c3
--- /dev/null
+++ b/app/views/api/v1/accounts/assignment_policies/show.json.jbuilder
@@ -0,0 +1 @@
+json.partial! 'assignment_policy', assignment_policy: @assignment_policy
diff --git a/app/views/api/v1/accounts/assignment_policies/update.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/update.json.jbuilder
new file mode 100644
index 000000000..8fd9543c3
--- /dev/null
+++ b/app/views/api/v1/accounts/assignment_policies/update.json.jbuilder
@@ -0,0 +1 @@
+json.partial! 'assignment_policy', assignment_policy: @assignment_policy
diff --git a/app/views/api/v1/accounts/inboxes/assignment_policies/create.json.jbuilder b/app/views/api/v1/accounts/inboxes/assignment_policies/create.json.jbuilder
new file mode 100644
index 000000000..105658704
--- /dev/null
+++ b/app/views/api/v1/accounts/inboxes/assignment_policies/create.json.jbuilder
@@ -0,0 +1 @@
+json.partial! 'api/v1/accounts/assignment_policies/assignment_policy', formats: [:json], assignment_policy: @assignment_policy
diff --git a/app/views/api/v1/accounts/inboxes/assignment_policies/show.json.jbuilder b/app/views/api/v1/accounts/inboxes/assignment_policies/show.json.jbuilder
new file mode 100644
index 000000000..105658704
--- /dev/null
+++ b/app/views/api/v1/accounts/inboxes/assignment_policies/show.json.jbuilder
@@ -0,0 +1 @@
+json.partial! 'api/v1/accounts/assignment_policies/assignment_policy', formats: [:json], assignment_policy: @assignment_policy
diff --git a/config/app.yml b/config/app.yml
index e6fc39be3..cc7233c18 100644
--- a/config/app.yml
+++ b/config/app.yml
@@ -1,5 +1,5 @@
shared: &shared
- version: '4.4.0'
+ version: '4.5.1'
development:
<<: *shared
diff --git a/config/application.rb b/config/application.rb
index 92dd9a011..3eca267f0 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -12,7 +12,7 @@ Bundler.require(*Rails.groups)
# We rely on DOTENV to load the environment variables
# We need these environment variables to load the specific APM agent
Dotenv::Rails.load
-require 'ddtrace' if ENV.fetch('DD_TRACE_AGENT_URL', false).present?
+require 'datadog' if ENV.fetch('DD_TRACE_AGENT_URL', false).present?
require 'elastic-apm' if ENV.fetch('ELASTIC_APM_SECRET_TOKEN', false).present?
require 'scout_apm' if ENV.fetch('SCOUT_KEY', false).present?
diff --git a/config/features.yml b/config/features.yml
index db2d46700..d6f2d24a3 100644
--- a/config/features.yml
+++ b/config/features.yml
@@ -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
diff --git a/config/locales/en.yml b/config/locales/en.yml
index c1f977549..9174437ac 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -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
diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml
index 7ddc6d31f..71ab85e3a 100644
--- a/config/locales/pt_BR.yml
+++ b/config/locales/pt_BR.yml
@@ -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'
diff --git a/config/routes.rb b/config/routes.rb
index a679782f4..fa0d51366 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -223,6 +223,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
diff --git a/db/migrate/20250808123008_add_feature_citation_to_assistant_config.rb b/db/migrate/20250808123008_add_feature_citation_to_assistant_config.rb
index e1f5c3f03..d5b037ec6 100644
--- a/db/migrate/20250808123008_add_feature_citation_to_assistant_config.rb
+++ b/db/migrate/20250808123008_add_feature_citation_to_assistant_config.rb
@@ -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')
diff --git a/enterprise/app/models/enterprise/concerns/assignment_policy.rb b/enterprise/app/models/enterprise/concerns/assignment_policy.rb
new file mode 100644
index 000000000..bddcc5e75
--- /dev/null
+++ b/enterprise/app/models/enterprise/concerns/assignment_policy.rb
@@ -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
diff --git a/package.json b/package.json
index 2aefb3d53..8dc6b2565 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@chatwoot/chatwoot",
- "version": "4.4.0",
+ "version": "4.5.1",
"license": "MIT",
"scripts": {
"eslint": "eslint app/**/*.{js,vue}",
diff --git a/spec/controllers/api/v1/accounts/assignment_policies/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/assignment_policies/inboxes_controller_spec.rb
new file mode 100644
index 000000000..6b3c677c5
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/assignment_policies/inboxes_controller_spec.rb
@@ -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
diff --git a/spec/controllers/api/v1/accounts/assignment_policies_controller_spec.rb b/spec/controllers/api/v1/accounts/assignment_policies_controller_spec.rb
new file mode 100644
index 000000000..f882ec992
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/assignment_policies_controller_spec.rb
@@ -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
diff --git a/spec/controllers/api/v1/accounts/inboxes/assignment_policies_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes/assignment_policies_controller_spec.rb
new file mode 100644
index 000000000..71ff464f8
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/inboxes/assignment_policies_controller_spec.rb
@@ -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
diff --git a/spec/enterprise/models/assignment_policy_spec.rb b/spec/enterprise/models/assignment_policy_spec.rb
new file mode 100644
index 000000000..0d21cc3c4
--- /dev/null
+++ b/spec/enterprise/models/assignment_policy_spec.rb
@@ -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
diff --git a/spec/factories/assignment_policies.rb b/spec/factories/assignment_policies.rb
new file mode 100644
index 000000000..6a696caa4
--- /dev/null
+++ b/spec/factories/assignment_policies.rb
@@ -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
diff --git a/spec/factories/inbox_assignment_policies.rb b/spec/factories/inbox_assignment_policies.rb
new file mode 100644
index 000000000..80bcae223
--- /dev/null
+++ b/spec/factories/inbox_assignment_policies.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :inbox_assignment_policy do
+ inbox
+ assignment_policy
+ end
+end
diff --git a/spec/models/assignment_policy_spec.rb b/spec/models/assignment_policy_spec.rb
new file mode 100644
index 000000000..1a97bbda0
--- /dev/null
+++ b/spec/models/assignment_policy_spec.rb
@@ -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