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:
parent
a14fd4ed83
commit
c954f0fab4
@ -121,6 +121,15 @@
|
|||||||
"RESET_SUCCESS": "Access token regenerated successfully",
|
"RESET_SUCCESS": "Access token regenerated successfully",
|
||||||
"RESET_ERROR": "Unable to regenerate access token. Please try again"
|
"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": {
|
"AUDIO_NOTIFICATIONS_SECTION": {
|
||||||
"TITLE": "Audio Alerts",
|
"TITLE": "Audio Alerts",
|
||||||
"NOTE": "Enable audio alerts in dashboard for new messages and conversations.",
|
"NOTE": "Enable audio alerts in dashboard for new messages and conversations.",
|
||||||
|
|||||||
@ -121,6 +121,15 @@
|
|||||||
"RESET_SUCCESS": "Token de acesso gerado novamente com sucesso",
|
"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"
|
"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": {
|
"AUDIO_NOTIFICATIONS_SECTION": {
|
||||||
"TITLE": "Alertas de áudio",
|
"TITLE": "Alertas de áudio",
|
||||||
"NOTE": "Habilitar notificações de áudio no painel para novas mensagens e conversas.",
|
"NOTE": "Habilitar notificações de áudio no painel para novas mensagens e conversas.",
|
||||||
|
|||||||
@ -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>
|
||||||
@ -17,6 +17,7 @@ import HotKeyCard from './HotKeyCard.vue';
|
|||||||
import ChangePassword from './ChangePassword.vue';
|
import ChangePassword from './ChangePassword.vue';
|
||||||
import NotificationPreferences from './NotificationPreferences.vue';
|
import NotificationPreferences from './NotificationPreferences.vue';
|
||||||
import AudioNotifications from './AudioNotifications.vue';
|
import AudioNotifications from './AudioNotifications.vue';
|
||||||
|
import AggressiveAlertSettings from './AggressiveAlertSettings.vue';
|
||||||
import FormSection from 'dashboard/components/FormSection.vue';
|
import FormSection from 'dashboard/components/FormSection.vue';
|
||||||
import AccessToken from './AccessToken.vue';
|
import AccessToken from './AccessToken.vue';
|
||||||
import MfaSettingsCard from './MfaSettingsCard.vue';
|
import MfaSettingsCard from './MfaSettingsCard.vue';
|
||||||
@ -40,6 +41,7 @@ export default {
|
|||||||
ChangePassword,
|
ChangePassword,
|
||||||
NotificationPreferences,
|
NotificationPreferences,
|
||||||
AudioNotifications,
|
AudioNotifications,
|
||||||
|
AggressiveAlertSettings,
|
||||||
AccessToken,
|
AccessToken,
|
||||||
MfaSettingsCard,
|
MfaSettingsCard,
|
||||||
AggressiveAlertProfileSetting,
|
AggressiveAlertProfileSetting,
|
||||||
@ -336,6 +338,22 @@ export default {
|
|||||||
<AudioNotifications />
|
<AudioNotifications />
|
||||||
</FormSection>
|
</FormSection>
|
||||||
</Policy>
|
</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">
|
<Policy :permissions="notificationPermissions">
|
||||||
<FormSection :title="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')">
|
<FormSection :title="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')">
|
||||||
<NotificationPreferences />
|
<NotificationPreferences />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user