feat(aggressive-alert): filtro por inbox configurável por admin
Admin configura em Account Settings → Agents → Editar quais inboxes disparam o banner agressivo (reopened + inatividade) pra cada agente. - user.ui_settings.aggressive_alert_inbox_ids: null (todas) | [] (nenhuma) | [1,2,3] - Filtro aplicado no actionCable.feedInactivityTracker, maybeTriggerAggressiveAlert e no hydrate do AggressiveConversationBanner - Backend aceita ui_settings no agents#update e serializa em _agent.json.jbuilder - UI no EditAgent com toggle "todas inboxes" + multiselect Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
49a21c845b
commit
b69fa21e53
@ -23,7 +23,9 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@agent.update!(agent_params.slice(:name).compact)
|
user_attrs = agent_params.slice(:name).compact
|
||||||
|
user_attrs[:ui_settings] = merged_ui_settings if agent_params[:ui_settings].present?
|
||||||
|
@agent.update!(user_attrs) if user_attrs.any?
|
||||||
@agent.current_account_user.update!(agent_params.slice(*account_user_attributes).compact)
|
@agent.current_account_user.update!(agent_params.slice(*account_user_attributes).compact)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -72,13 +74,19 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def allowed_agent_params
|
def allowed_agent_params
|
||||||
[:name, :email, :role, :availability, :auto_offline]
|
[:name, :email, :role, :availability, :auto_offline, { ui_settings: [:aggressive_alert_inbox_ids_mode, { aggressive_alert_inbox_ids: [] }] }]
|
||||||
end
|
end
|
||||||
|
|
||||||
def agent_params
|
def agent_params
|
||||||
params.require(:agent).permit(allowed_agent_params)
|
params.require(:agent).permit(allowed_agent_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def merged_ui_settings
|
||||||
|
existing = @agent.ui_settings || {}
|
||||||
|
incoming = agent_params[:ui_settings].to_h.deep_stringify_keys
|
||||||
|
existing.merge(incoming)
|
||||||
|
end
|
||||||
|
|
||||||
def new_agent_params
|
def new_agent_params
|
||||||
params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
|
params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
|
||||||
end
|
end
|
||||||
|
|||||||
@ -17,7 +17,18 @@ export default {
|
|||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentAccountId: 'getCurrentAccountId',
|
currentAccountId: 'getCurrentAccountId',
|
||||||
allConversations: 'getAllConversations',
|
allConversations: 'getAllConversations',
|
||||||
|
currentUser: 'getCurrentUser',
|
||||||
}),
|
}),
|
||||||
|
allowedInboxIds() {
|
||||||
|
// null → sem filtro (todas); array → só essas.
|
||||||
|
const raw =
|
||||||
|
this.currentUser &&
|
||||||
|
this.currentUser.ui_settings &&
|
||||||
|
this.currentUser.ui_settings.aggressive_alert_inbox_ids;
|
||||||
|
if (raw == null) return null;
|
||||||
|
if (!Array.isArray(raw)) return null;
|
||||||
|
return raw.map(id => Number(id));
|
||||||
|
},
|
||||||
hasAlerts() {
|
hasAlerts() {
|
||||||
return this.alerts.length > 0;
|
return this.alerts.length > 0;
|
||||||
},
|
},
|
||||||
@ -78,7 +89,14 @@ export default {
|
|||||||
// quando o usuário só abriu a aba sem receber mensagem ao vivo.
|
// quando o usuário só abriu a aba sem receber mensagem ao vivo.
|
||||||
allConversations: {
|
allConversations: {
|
||||||
handler(conversations) {
|
handler(conversations) {
|
||||||
inactivityAlertTracker.hydrateFromConversations(conversations);
|
const allowed = this.allowedInboxIds;
|
||||||
|
const filtered =
|
||||||
|
allowed === null
|
||||||
|
? conversations
|
||||||
|
: (conversations || []).filter(c =>
|
||||||
|
allowed.includes(Number(c && c.inbox_id))
|
||||||
|
);
|
||||||
|
inactivityAlertTracker.hydrateFromConversations(filtered);
|
||||||
},
|
},
|
||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -133,12 +133,14 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
const isIncoming = messageType === 0 || messageType === 'incoming';
|
const isIncoming = messageType === 0 || messageType === 'incoming';
|
||||||
const conversationStatus = conversation && conversation.status;
|
const conversationStatus = conversation && conversation.status;
|
||||||
if (isIncoming && conversationStatus === 'open') {
|
if (isIncoming && conversationStatus === 'open') {
|
||||||
|
const inboxId = conversation && conversation.inbox_id;
|
||||||
|
if (!this.isInboxAllowedForUser(inboxId)) return;
|
||||||
const contactName =
|
const contactName =
|
||||||
conversation && conversation.meta && conversation.meta.sender
|
conversation && conversation.meta && conversation.meta.sender
|
||||||
? conversation.meta.sender.name
|
? conversation.meta.sender.name
|
||||||
: '';
|
: '';
|
||||||
const inbox = this.app.$store.getters['inboxes/getInbox']
|
const inbox = this.app.$store.getters['inboxes/getInbox']
|
||||||
? this.app.$store.getters['inboxes/getInbox'](conversation.inbox_id)
|
? this.app.$store.getters['inboxes/getInbox'](inboxId)
|
||||||
: null;
|
: null;
|
||||||
const inboxName = inbox && inbox.name ? inbox.name : '';
|
const inboxName = inbox && inbox.name ? inbox.name : '';
|
||||||
inactivityAlertTracker.onClientMessage({
|
inactivityAlertTracker.onClientMessage({
|
||||||
@ -174,6 +176,22 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
return accountEnabled && userEnabled;
|
return accountEnabled && userEnabled;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Filtra alertas por inbox conforme a preferência do user.
|
||||||
|
// ui_settings.aggressive_alert_inbox_ids:
|
||||||
|
// - null/undefined → todas as inboxes (default, legado)
|
||||||
|
// - [] (vazio) → nenhuma inbox (silenciou tudo)
|
||||||
|
// - [1, 2, 3] → só essas inboxes
|
||||||
|
isInboxAllowedForUser = inboxId => {
|
||||||
|
if (inboxId == null) return true;
|
||||||
|
const user = this.app.$store.getters.getCurrentUser;
|
||||||
|
const allowed =
|
||||||
|
user && user.ui_settings && user.ui_settings.aggressive_alert_inbox_ids;
|
||||||
|
if (allowed == null) return true;
|
||||||
|
if (!Array.isArray(allowed)) return true;
|
||||||
|
// Inbox ids podem vir como number no evento e string no ui_settings.
|
||||||
|
return allowed.some(id => Number(id) === Number(inboxId));
|
||||||
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
onReload = () => window.location.reload();
|
onReload = () => window.location.reload();
|
||||||
|
|
||||||
@ -194,6 +212,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
maybeTriggerAggressiveAlert = data => {
|
maybeTriggerAggressiveAlert = data => {
|
||||||
if (!data || data.status !== 'open') return;
|
if (!data || data.status !== 'open') return;
|
||||||
if (!this.isAggressiveAlertEnabled()) return;
|
if (!this.isAggressiveAlertEnabled()) return;
|
||||||
|
if (!this.isInboxAllowedForUser(data.inbox_id)) return;
|
||||||
const store = this.app.$store;
|
const store = this.app.$store;
|
||||||
const contactName =
|
const contactName =
|
||||||
data.meta && data.meta.sender ? data.meta.sender.name : '';
|
data.meta && data.meta.sender ? data.meta.sender.name : '';
|
||||||
|
|||||||
@ -94,6 +94,13 @@
|
|||||||
"ADMIN_SUCCESS_MESSAGE": "An email with reset password instructions has been sent to the agent",
|
"ADMIN_SUCCESS_MESSAGE": "An email with reset password instructions has been sent to the agent",
|
||||||
"SUCCESS_MESSAGE": "Agent password reset successfully",
|
"SUCCESS_MESSAGE": "Agent password reset successfully",
|
||||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||||
|
},
|
||||||
|
"AGGRESSIVE_ALERT": {
|
||||||
|
"LABEL": "Aggressive alert — inboxes",
|
||||||
|
"DESCRIPTION": "Choose which inboxes will trigger the reopened/inactivity banner for this agent.",
|
||||||
|
"ALL_INBOXES": "All inboxes",
|
||||||
|
"PICK_INBOXES": "Select inboxes",
|
||||||
|
"NONE_WARNING": "No inbox selected — this agent will not see the aggressive alert."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SEARCH": {
|
"SEARCH": {
|
||||||
|
|||||||
@ -94,6 +94,13 @@
|
|||||||
"ADMIN_SUCCESS_MESSAGE": "Um e-mail com instruções de redefinição de senha foi enviado para o agente",
|
"ADMIN_SUCCESS_MESSAGE": "Um e-mail com instruções de redefinição de senha foi enviado para o agente",
|
||||||
"SUCCESS_MESSAGE": "Senha do agente redefinida com sucesso",
|
"SUCCESS_MESSAGE": "Senha do agente redefinida com sucesso",
|
||||||
"ERROR_MESSAGE": "Não foi possível conectar ao servidor Woot, por favor tente novamente mais tarde"
|
"ERROR_MESSAGE": "Não foi possível conectar ao servidor Woot, por favor tente novamente mais tarde"
|
||||||
|
},
|
||||||
|
"AGGRESSIVE_ALERT": {
|
||||||
|
"LABEL": "Alerta agressivo — caixas de entrada",
|
||||||
|
"DESCRIPTION": "Escolha em quais caixas de entrada este agente verá o banner de conversa reaberta e de inatividade.",
|
||||||
|
"ALL_INBOXES": "Em todas as caixas de entrada",
|
||||||
|
"PICK_INBOXES": "Selecione as caixas de entrada",
|
||||||
|
"NONE_WARNING": "Nenhuma caixa selecionada — este agente não verá o alerta agressivo."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SEARCH": {
|
"SEARCH": {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useStore, useMapGetter } from 'dashboard/composables/store';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Multiselect from 'vue-multiselect';
|
||||||
import Auth from '../../../../api/auth';
|
import Auth from '../../../../api/auth';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
|
|
||||||
@ -38,6 +39,10 @@ const props = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
uiSettings: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
@ -52,6 +57,27 @@ const agentAvailability = ref(props.availability);
|
|||||||
const selectedRoleId = ref(props.customRoleId || props.type);
|
const selectedRoleId = ref(props.customRoleId || props.type);
|
||||||
const agentCredentials = ref({ email: props.email });
|
const agentCredentials = ref({ email: props.email });
|
||||||
|
|
||||||
|
// --- Alerta agressivo por inbox -------------------------------------------
|
||||||
|
// ui_settings.aggressive_alert_inbox_ids:
|
||||||
|
// null/undefined → todas (default, legado)
|
||||||
|
// [] → nenhuma (silenciou tudo)
|
||||||
|
// [1, 2, 3] → só essas
|
||||||
|
const initialInboxIds = props.uiSettings?.aggressive_alert_inbox_ids;
|
||||||
|
const alertAllInboxes = ref(
|
||||||
|
initialInboxIds === null ||
|
||||||
|
initialInboxIds === undefined ||
|
||||||
|
!Array.isArray(initialInboxIds)
|
||||||
|
);
|
||||||
|
|
||||||
|
const inboxes = useMapGetter('inboxes/getInboxes');
|
||||||
|
const selectedAlertInboxes = ref(
|
||||||
|
Array.isArray(initialInboxIds) && inboxes.value
|
||||||
|
? inboxes.value.filter(i =>
|
||||||
|
initialInboxIds.map(id => Number(id)).includes(Number(i.id))
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
agentName: { required, minLength: minLength(1) },
|
agentName: { required, minLength: minLength(1) },
|
||||||
selectedRoleId: { required },
|
selectedRoleId: { required },
|
||||||
@ -135,6 +161,12 @@ const editAgent = async () => {
|
|||||||
payload.custom_role_id = null;
|
payload.custom_role_id = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
payload.ui_settings = {
|
||||||
|
aggressive_alert_inbox_ids: alertAllInboxes.value
|
||||||
|
? null
|
||||||
|
: selectedAlertInboxes.value.map(i => Number(i.id)),
|
||||||
|
};
|
||||||
|
|
||||||
await store.dispatch('agents/update', payload);
|
await store.dispatch('agents/update', payload);
|
||||||
useAlert(t('AGENT_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
useAlert(t('AGENT_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||||
emit('close');
|
emit('close');
|
||||||
@ -204,6 +236,47 @@ const resetPassword = async () => {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="mt-2 border-t pt-3 border-n-weak">
|
||||||
|
<span class="block font-medium mb-1">
|
||||||
|
{{ $t('AGENT_MGMT.EDIT.AGGRESSIVE_ALERT.LABEL') }}
|
||||||
|
</span>
|
||||||
|
<p class="text-xs text-n-slate-11 mb-2">
|
||||||
|
{{ $t('AGENT_MGMT.EDIT.AGGRESSIVE_ALERT.DESCRIPTION') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<label class="flex items-center gap-2 mb-2">
|
||||||
|
<input v-model="alertAllInboxes" type="checkbox" class="!m-0" />
|
||||||
|
<span>
|
||||||
|
{{ $t('AGENT_MGMT.EDIT.AGGRESSIVE_ALERT.ALL_INBOXES') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div v-if="!alertAllInboxes">
|
||||||
|
<Multiselect
|
||||||
|
v-model="selectedAlertInboxes"
|
||||||
|
:options="inboxes || []"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
multiple
|
||||||
|
:close-on-select="false"
|
||||||
|
:clear-on-select="false"
|
||||||
|
hide-selected
|
||||||
|
:placeholder="$t('AGENT_MGMT.EDIT.AGGRESSIVE_ALERT.PICK_INBOXES')"
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
:deselect-label="$t('FORMS.MULTISELECT.ENTER_TO_REMOVE')"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
v-if="selectedAlertInboxes.length === 0"
|
||||||
|
class="text-xs text-n-slate-11 mt-1"
|
||||||
|
>
|
||||||
|
{{ $t('AGENT_MGMT.EDIT.AGGRESSIVE_ALERT.NONE_WARNING') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row justify-start w-full gap-2 px-0 py-2">
|
<div class="flex flex-row justify-start w-full gap-2 px-0 py-2">
|
||||||
<div class="w-[50%] ltr:text-left rtl:text-right">
|
<div class="w-[50%] ltr:text-left rtl:text-right">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -266,6 +266,7 @@ const confirmDeletion = () => {
|
|||||||
:email="currentAgent.email"
|
:email="currentAgent.email"
|
||||||
:availability="currentAgent.availability_status"
|
:availability="currentAgent.availability_status"
|
||||||
:custom-role-id="currentAgent.custom_role_id"
|
:custom-role-id="currentAgent.custom_role_id"
|
||||||
|
:ui-settings="currentAgent.ui_settings || {}"
|
||||||
@close="hideEditPopup"
|
@close="hideEditPopup"
|
||||||
/>
|
/>
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
|
|||||||
@ -11,4 +11,5 @@ json.custom_attributes resource.custom_attributes if resource.custom_attributes.
|
|||||||
json.name resource.name
|
json.name resource.name
|
||||||
json.role resource.role
|
json.role resource.role
|
||||||
json.thumbnail resource.avatar_url
|
json.thumbnail resource.avatar_url
|
||||||
|
json.ui_settings resource.ui_settings
|
||||||
json.custom_role_id resource.current_account_user&.custom_role_id if ChatwootApp.enterprise?
|
json.custom_role_id resource.current_account_user&.custom_role_id if ChatwootApp.enterprise?
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user