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
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
@ -72,13 +74,19 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def agent_params
|
||||
params.require(:agent).permit(allowed_agent_params)
|
||||
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
|
||||
params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
|
||||
end
|
||||
|
||||
@ -17,7 +17,18 @@ export default {
|
||||
...mapGetters({
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
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() {
|
||||
return this.alerts.length > 0;
|
||||
},
|
||||
@ -78,7 +89,14 @@ export default {
|
||||
// quando o usuário só abriu a aba sem receber mensagem ao vivo.
|
||||
allConversations: {
|
||||
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,
|
||||
},
|
||||
|
||||
@ -133,12 +133,14 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
const isIncoming = messageType === 0 || messageType === 'incoming';
|
||||
const conversationStatus = conversation && conversation.status;
|
||||
if (isIncoming && conversationStatus === 'open') {
|
||||
const inboxId = conversation && conversation.inbox_id;
|
||||
if (!this.isInboxAllowedForUser(inboxId)) return;
|
||||
const contactName =
|
||||
conversation && conversation.meta && conversation.meta.sender
|
||||
? conversation.meta.sender.name
|
||||
: '';
|
||||
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;
|
||||
const inboxName = inbox && inbox.name ? inbox.name : '';
|
||||
inactivityAlertTracker.onClientMessage({
|
||||
@ -174,6 +176,22 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
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
|
||||
onReload = () => window.location.reload();
|
||||
|
||||
@ -194,6 +212,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
maybeTriggerAggressiveAlert = data => {
|
||||
if (!data || data.status !== 'open') return;
|
||||
if (!this.isAggressiveAlertEnabled()) return;
|
||||
if (!this.isInboxAllowedForUser(data.inbox_id)) return;
|
||||
const store = this.app.$store;
|
||||
const contactName =
|
||||
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",
|
||||
"SUCCESS_MESSAGE": "Agent password reset successfully",
|
||||
"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": {
|
||||
|
||||
@ -94,6 +94,13 @@
|
||||
"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",
|
||||
"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": {
|
||||
|
||||
@ -6,6 +6,7 @@ import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import Auth from '../../../../api/auth';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
@ -38,6 +39,10 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
uiSettings: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
@ -52,6 +57,27 @@ const agentAvailability = ref(props.availability);
|
||||
const selectedRoleId = ref(props.customRoleId || props.type);
|
||||
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 = {
|
||||
agentName: { required, minLength: minLength(1) },
|
||||
selectedRoleId: { required },
|
||||
@ -135,6 +161,12 @@ const editAgent = async () => {
|
||||
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);
|
||||
useAlert(t('AGENT_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||
emit('close');
|
||||
@ -204,6 +236,47 @@ const resetPassword = async () => {
|
||||
</label>
|
||||
</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="w-[50%] ltr:text-left rtl:text-right">
|
||||
<Button
|
||||
|
||||
@ -266,6 +266,7 @@ const confirmDeletion = () => {
|
||||
:email="currentAgent.email"
|
||||
:availability="currentAgent.availability_status"
|
||||
:custom-role-id="currentAgent.custom_role_id"
|
||||
:ui-settings="currentAgent.ui_settings || {}"
|
||||
@close="hideEditPopup"
|
||||
/>
|
||||
</woot-modal>
|
||||
|
||||
@ -11,4 +11,5 @@ json.custom_attributes resource.custom_attributes if resource.custom_attributes.
|
||||
json.name resource.name
|
||||
json.role resource.role
|
||||
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?
|
||||
|
||||
Loading…
Reference in New Issue
Block a user