feat(profile): UI pra ligar/desligar alerta de conversa parada

Nova seção em Configurações → Perfil "Alerta de conversa parada" com:
- Checkbox principal "Ativar alerta de conversa parada" (OFF salva
  ui_settings.aggressive_alert_inbox_ids = []).
- Sub-checkbox "Aplicar em todas as caixas" (ON salva null = todas).
- Lista de inboxes (visível quando não é "todas") pra selecionar caso
  por caso (salva [id, id, ...]).
- Persiste a cada change via updateUISettings (sem botão "salvar").

Antes só dava pra mexer via Rails runner. Cada admin agora controla
sozinho sem mexer em DB.

i18n: PROFILE_SETTINGS.FORM.AGGRESSIVE_ALERT_SECTION.* em pt_BR e en.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rodribm10 2026-05-02 22:04:14 -03:00
parent a14fd4ed83
commit c954f0fab4
4 changed files with 269 additions and 0 deletions

View File

@ -121,6 +121,15 @@
"RESET_SUCCESS": "Access token regenerated successfully",
"RESET_ERROR": "Unable to regenerate access token. Please try again"
},
"AGGRESSIVE_ALERT_SECTION": {
"TITLE": "Stalled conversation alert",
"NOTE": "Red banner that appears at the top of the panel when a conversation has been waiting for a reply for 5+ minutes.",
"DESCRIPTION": "Red banner shown when a conversation has no reply for 5+ minutes. Useful to avoid losing customers, but can be intrusive if you don't handle every inbox.",
"ENABLED": "Enable stalled conversation alert",
"APPLY_TO_ALL": "Apply to all inboxes",
"INBOX_HINT": "Pick the inboxes where you want to receive the alert:",
"NO_INBOXES": "No inboxes registered."
},
"AUDIO_NOTIFICATIONS_SECTION": {
"TITLE": "Audio Alerts",
"NOTE": "Enable audio alerts in dashboard for new messages and conversations.",

View File

@ -121,6 +121,15 @@
"RESET_SUCCESS": "Token de acesso gerado novamente com sucesso",
"RESET_ERROR": "Não foi possível regerar o token de acesso. Por favor, tente novamente"
},
"AGGRESSIVE_ALERT_SECTION": {
"TITLE": "Alerta de conversa parada",
"NOTE": "Banner vermelho que aparece no topo do painel quando uma conversa fica sem resposta há 5+ minutos.",
"DESCRIPTION": "Banner vermelho que aparece quando uma conversa fica sem resposta há 5+ minutos. Útil pra não perder cliente, mas pode ser intrusivo se você não atende todas as inboxes.",
"ENABLED": "Ativar alerta de conversa parada",
"APPLY_TO_ALL": "Aplicar em todas as caixas de entrada",
"INBOX_HINT": "Selecione as caixas onde você quer receber o alerta:",
"NO_INBOXES": "Nenhuma caixa de entrada cadastrada."
},
"AUDIO_NOTIFICATIONS_SECTION": {
"TITLE": "Alertas de áudio",
"NOTE": "Habilitar notificações de áudio no painel para novas mensagens e conversas.",

View File

@ -0,0 +1,233 @@
<script setup>
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { computed, ref, watch } from 'vue';
import { useStoreGetters } from 'dashboard/composables/store';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const getters = useStoreGetters();
const { uiSettings, updateUISettings } = useUISettings();
const inboxes = computed(() => getters['inboxes/getInboxes'].value || []);
// Modelo: ui_settings.aggressive_alert_inbox_ids
// undefined / null todas as inboxes (default histórico)
// [] desligado pra esse usuário
// [id, id, ...] apenas essas inboxes
const enabled = ref(true);
const selectedInboxIds = ref([]);
const applyToAll = ref(true);
const initFromSettings = settings => {
const raw = settings?.aggressive_alert_inbox_ids;
if (Array.isArray(raw)) {
if (raw.length === 0) {
enabled.value = false;
applyToAll.value = true;
selectedInboxIds.value = [];
} else {
enabled.value = true;
applyToAll.value = false;
selectedInboxIds.value = raw.map(id => Number(id));
}
} else {
enabled.value = true;
applyToAll.value = true;
selectedInboxIds.value = [];
}
};
watch(
uiSettings,
value => {
initFromSettings(value);
},
{ immediate: true }
);
const persist = async () => {
let value;
if (!enabled.value) {
value = [];
} else if (applyToAll.value) {
value = null;
} else {
value = selectedInboxIds.value.map(id => Number(id));
}
try {
await updateUISettings({ aggressive_alert_inbox_ids: value });
useAlert(t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
} catch (e) {
useAlert(t('PROFILE_SETTINGS.FORM.API.UPDATE_ERROR'));
}
};
const handleEnabledChange = event => {
enabled.value = event.target.checked;
persist();
};
const handleApplyToAllChange = event => {
applyToAll.value = event.target.checked;
if (applyToAll.value) {
selectedInboxIds.value = [];
}
persist();
};
const handleInboxToggle = inboxId => {
const id = Number(inboxId);
if (selectedInboxIds.value.includes(id)) {
selectedInboxIds.value = selectedInboxIds.value.filter(i => i !== id);
} else {
selectedInboxIds.value = [...selectedInboxIds.value, id];
}
persist();
};
const isInboxSelected = inboxId =>
selectedInboxIds.value.includes(Number(inboxId));
</script>
<template>
<div class="aggressive-alert-settings flex flex-col gap-4">
<p class="description">
{{
$t(
'PROFILE_SETTINGS.FORM.AGGRESSIVE_ALERT_SECTION.DESCRIPTION',
'Banner vermelho que aparece quando uma conversa fica sem resposta há 5+ minutos. Útil pra não perder cliente, mas pode ser intrusivo se você não atende todas as inboxes.'
)
}}
</p>
<label class="toggle-row">
<input
type="checkbox"
:checked="enabled"
class="toggle-input"
@change="handleEnabledChange"
/>
<span class="toggle-label">
{{
$t(
'PROFILE_SETTINGS.FORM.AGGRESSIVE_ALERT_SECTION.ENABLED',
'Ativar alerta de conversa parada'
)
}}
</span>
</label>
<div v-if="enabled" class="scope-section">
<label class="toggle-row">
<input
type="checkbox"
:checked="applyToAll"
class="toggle-input"
@change="handleApplyToAllChange"
/>
<span class="toggle-label">
{{
$t(
'PROFILE_SETTINGS.FORM.AGGRESSIVE_ALERT_SECTION.APPLY_TO_ALL',
'Aplicar em todas as caixas de entrada'
)
}}
</span>
</label>
<div v-if="!applyToAll" class="inbox-list">
<p class="hint">
{{
$t(
'PROFILE_SETTINGS.FORM.AGGRESSIVE_ALERT_SECTION.INBOX_HINT',
'Selecione as caixas onde você quer receber o alerta:'
)
}}
</p>
<label v-for="inbox in inboxes" :key="inbox.id" class="inbox-row">
<input
type="checkbox"
:checked="isInboxSelected(inbox.id)"
class="toggle-input"
@change="handleInboxToggle(inbox.id)"
/>
<span>{{ inbox.name }}</span>
</label>
<p v-if="!inboxes.length" class="empty">
{{
$t(
'PROFILE_SETTINGS.FORM.AGGRESSIVE_ALERT_SECTION.NO_INBOXES',
'Nenhuma caixa de entrada cadastrada.'
)
}}
</p>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.aggressive-alert-settings {
max-width: 480px;
}
.description {
color: var(--color-text-light, #6b7280);
font-size: 13px;
line-height: 1.5;
margin: 0;
}
.toggle-row {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
}
.toggle-input {
cursor: pointer;
}
.toggle-label {
font-size: 14px;
font-weight: 500;
}
.scope-section {
margin-left: 24px;
display: flex;
flex-direction: column;
gap: 8px;
}
.inbox-list {
margin-left: 24px;
display: flex;
flex-direction: column;
gap: 6px;
padding-top: 4px;
}
.inbox-row {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 13px;
}
.hint {
font-size: 12px;
color: var(--color-text-light, #6b7280);
margin: 0;
}
.empty {
font-size: 12px;
color: var(--color-text-light, #9ca3af);
font-style: italic;
}
</style>

View File

@ -17,6 +17,7 @@ import HotKeyCard from './HotKeyCard.vue';
import ChangePassword from './ChangePassword.vue';
import NotificationPreferences from './NotificationPreferences.vue';
import AudioNotifications from './AudioNotifications.vue';
import AggressiveAlertSettings from './AggressiveAlertSettings.vue';
import FormSection from 'dashboard/components/FormSection.vue';
import AccessToken from './AccessToken.vue';
import MfaSettingsCard from './MfaSettingsCard.vue';
@ -40,6 +41,7 @@ export default {
ChangePassword,
NotificationPreferences,
AudioNotifications,
AggressiveAlertSettings,
AccessToken,
MfaSettingsCard,
AggressiveAlertProfileSetting,
@ -336,6 +338,22 @@ export default {
<AudioNotifications />
</FormSection>
</Policy>
<FormSection
:title="
$t(
'PROFILE_SETTINGS.FORM.AGGRESSIVE_ALERT_SECTION.TITLE',
'Alerta de conversa parada'
)
"
:description="
$t(
'PROFILE_SETTINGS.FORM.AGGRESSIVE_ALERT_SECTION.NOTE',
'Banner vermelho que aparece no topo do painel quando uma conversa fica sem resposta há 5+ minutos.'
)
"
>
<AggressiveAlertSettings />
</FormSection>
<Policy :permissions="notificationPermissions">
<FormSection :title="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')">
<NotificationPreferences />