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>
376 lines
12 KiB
Vue
376 lines
12 KiB
Vue
<script>
|
|
import { mapGetters } from 'vuex';
|
|
import { useAlert } from 'dashboard/composables';
|
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
|
import { useFontSize } from 'dashboard/composables/useFontSize';
|
|
import { useBranding } from 'shared/composables/useBranding';
|
|
import { clearCookiesOnLogout } from 'dashboard/store/utils/api.js';
|
|
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
|
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
|
|
import { parseBoolean } from '@chatwoot/utils';
|
|
import UserProfilePicture from './UserProfilePicture.vue';
|
|
import UserBasicDetails from './UserBasicDetails.vue';
|
|
import MessageSignature from './MessageSignature.vue';
|
|
import FontSize from './FontSize.vue';
|
|
import UserLanguageSelect from './UserLanguageSelect.vue';
|
|
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';
|
|
import AggressiveAlertProfileSetting from './AggressiveAlertProfileSetting.vue';
|
|
import Policy from 'dashboard/components/policy.vue';
|
|
import {
|
|
ROLES,
|
|
CONVERSATION_PERMISSIONS,
|
|
} from 'dashboard/constants/permissions.js';
|
|
|
|
export default {
|
|
components: {
|
|
MessageSignature,
|
|
FormSection,
|
|
FontSize,
|
|
UserLanguageSelect,
|
|
UserProfilePicture,
|
|
Policy,
|
|
UserBasicDetails,
|
|
HotKeyCard,
|
|
ChangePassword,
|
|
NotificationPreferences,
|
|
AudioNotifications,
|
|
AggressiveAlertSettings,
|
|
AccessToken,
|
|
MfaSettingsCard,
|
|
AggressiveAlertProfileSetting,
|
|
},
|
|
setup() {
|
|
const { isEditorHotKeyEnabled, updateUISettings } = useUISettings();
|
|
const { currentFontSize, updateFontSize } = useFontSize();
|
|
const { replaceInstallationName } = useBranding();
|
|
|
|
return {
|
|
currentFontSize,
|
|
updateFontSize,
|
|
isEditorHotKeyEnabled,
|
|
updateUISettings,
|
|
replaceInstallationName,
|
|
};
|
|
},
|
|
data() {
|
|
return {
|
|
avatarFile: '',
|
|
avatarUrl: '',
|
|
name: '',
|
|
displayName: '',
|
|
email: '',
|
|
messageSignature: '',
|
|
signaturePosition: '',
|
|
signatureSeparator: '',
|
|
hotKeys: [
|
|
{
|
|
key: 'enter',
|
|
title: this.$t(
|
|
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.HEADING'
|
|
),
|
|
description: this.$t(
|
|
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.CONTENT'
|
|
),
|
|
lightImage: '/assets/images/dashboard/profile/hot-key-enter.svg',
|
|
darkImage: '/assets/images/dashboard/profile/hot-key-enter-dark.svg',
|
|
},
|
|
{
|
|
key: 'cmd_enter',
|
|
title: this.$t(
|
|
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.HEADING'
|
|
),
|
|
description: this.$t(
|
|
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.CONTENT'
|
|
),
|
|
lightImage: '/assets/images/dashboard/profile/hot-key-ctrl-enter.svg',
|
|
darkImage:
|
|
'/assets/images/dashboard/profile/hot-key-ctrl-enter-dark.svg',
|
|
},
|
|
],
|
|
notificationPermissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
|
audioNotificationPermissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
|
};
|
|
},
|
|
computed: {
|
|
...mapGetters({
|
|
currentUser: 'getCurrentUser',
|
|
currentUserId: 'getCurrentUserID',
|
|
globalConfig: 'globalConfig/get',
|
|
}),
|
|
isMfaEnabled() {
|
|
return parseBoolean(window.chatwootConfig?.isMfaEnabled);
|
|
},
|
|
},
|
|
mounted() {
|
|
if (this.currentUserId) {
|
|
this.initializeUser();
|
|
}
|
|
},
|
|
methods: {
|
|
initializeUser() {
|
|
this.name = this.currentUser.name;
|
|
this.email = this.currentUser.email;
|
|
this.avatarUrl = this.currentUser.avatar_url;
|
|
this.displayName = this.currentUser.display_name;
|
|
this.messageSignature = this.currentUser.message_signature;
|
|
|
|
const { signature_position, signature_separator } =
|
|
this.currentUser.ui_settings || {};
|
|
this.signaturePosition = signature_position || 'top';
|
|
this.signatureSeparator = signature_separator || 'blank';
|
|
},
|
|
async dispatchUpdate(payload, successMessage, errorMessage) {
|
|
let alertMessage = '';
|
|
try {
|
|
await this.$store.dispatch('updateProfile', payload);
|
|
alertMessage = successMessage;
|
|
|
|
return true; // return the value so that the status can be known
|
|
} catch (error) {
|
|
alertMessage = parseAPIErrorResponse(error) || errorMessage;
|
|
|
|
return false; // return the value so that the status can be known
|
|
} finally {
|
|
useAlert(alertMessage);
|
|
}
|
|
},
|
|
async updateProfile(userAttributes) {
|
|
const { name, email, displayName } = userAttributes;
|
|
const hasEmailChanged = this.currentUser.email !== email;
|
|
this.name = name || this.name;
|
|
this.email = email || this.email;
|
|
this.displayName = displayName || this.displayName;
|
|
|
|
const updatePayload = {
|
|
name: this.name,
|
|
email: this.email,
|
|
displayName: this.displayName,
|
|
avatar: this.avatarFile,
|
|
};
|
|
|
|
const success = await this.dispatchUpdate(
|
|
updatePayload,
|
|
hasEmailChanged
|
|
? this.$t('PROFILE_SETTINGS.AFTER_EMAIL_CHANGED')
|
|
: this.$t('PROFILE_SETTINGS.UPDATE_SUCCESS'),
|
|
this.$t('RESET_PASSWORD.API.ERROR_MESSAGE')
|
|
);
|
|
|
|
if (hasEmailChanged && success) clearCookiesOnLogout();
|
|
},
|
|
async updateSignature(signature, signaturePosition, signatureSeparator) {
|
|
try {
|
|
const signaturePayload = { message_signature: signature };
|
|
await this.dispatchUpdate(
|
|
signaturePayload,
|
|
this.$t(
|
|
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_SUCCESS'
|
|
),
|
|
this.$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_ERROR')
|
|
);
|
|
|
|
await this.updateUISettings({
|
|
signature_position: signaturePosition,
|
|
signature_separator: signatureSeparator,
|
|
});
|
|
|
|
this.signaturePosition = signaturePosition;
|
|
this.signatureSeparator = signatureSeparator;
|
|
} catch (error) {
|
|
useAlert(
|
|
this.$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_ERROR')
|
|
);
|
|
}
|
|
},
|
|
updateProfilePicture({ file, url }) {
|
|
this.avatarFile = file;
|
|
this.avatarUrl = url;
|
|
},
|
|
async deleteProfilePicture() {
|
|
try {
|
|
await this.$store.dispatch('deleteAvatar');
|
|
this.avatarUrl = '';
|
|
this.avatarFile = '';
|
|
useAlert(this.$t('PROFILE_SETTINGS.AVATAR_DELETE_SUCCESS'));
|
|
} catch (error) {
|
|
useAlert(this.$t('PROFILE_SETTINGS.AVATAR_DELETE_FAILED'));
|
|
}
|
|
},
|
|
toggleHotKey(key) {
|
|
this.hotKeys = this.hotKeys.map(hotKey =>
|
|
hotKey.key === key ? { ...hotKey, active: !hotKey.active } : hotKey
|
|
);
|
|
this.updateUISettings({ editor_message_key: key });
|
|
useAlert(this.$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.UPDATE_SUCCESS'));
|
|
},
|
|
async onCopyToken(value) {
|
|
await copyTextToClipboard(value);
|
|
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
|
},
|
|
async resetAccessToken() {
|
|
const success = await this.$store.dispatch('resetAccessToken');
|
|
if (success) {
|
|
useAlert(this.$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.RESET_SUCCESS'));
|
|
} else {
|
|
useAlert(this.$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.RESET_ERROR'));
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="grid py-16 px-5 font-inter mx-auto gap-16 sm:max-w-screen-md">
|
|
<div class="flex flex-col gap-6">
|
|
<h2 class="text-2xl font-medium text-n-slate-12">
|
|
{{ $t('PROFILE_SETTINGS.TITLE') }}
|
|
</h2>
|
|
<UserProfilePicture
|
|
:src="avatarUrl"
|
|
:name="name"
|
|
@change="updateProfilePicture"
|
|
@delete="deleteProfilePicture"
|
|
/>
|
|
<UserBasicDetails
|
|
:name="name"
|
|
:display-name="displayName"
|
|
:email="email"
|
|
:email-enabled="!globalConfig.disableUserProfileUpdate"
|
|
@update-user="updateProfile"
|
|
/>
|
|
</div>
|
|
<FormSection
|
|
:title="$t('PROFILE_SETTINGS.FORM.AGGRESSIVE_ALERT.SECTION_TITLE')"
|
|
:description="$t('PROFILE_SETTINGS.FORM.AGGRESSIVE_ALERT.SECTION_NOTE')"
|
|
>
|
|
<AggressiveAlertProfileSetting />
|
|
</FormSection>
|
|
<FormSection
|
|
:title="$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.TITLE')"
|
|
:description="
|
|
replaceInstallationName(
|
|
$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.NOTE')
|
|
)
|
|
"
|
|
>
|
|
<FontSize
|
|
:value="currentFontSize"
|
|
:label="$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.TITLE')"
|
|
:description="
|
|
$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.NOTE')
|
|
"
|
|
@change="updateFontSize"
|
|
/>
|
|
<UserLanguageSelect
|
|
:label="$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.LANGUAGE.TITLE')"
|
|
:description="
|
|
$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.LANGUAGE.NOTE')
|
|
"
|
|
/>
|
|
</FormSection>
|
|
<FormSection
|
|
:title="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.TITLE')"
|
|
:description="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.NOTE')"
|
|
>
|
|
<MessageSignature
|
|
:message-signature="messageSignature"
|
|
:signature-position="signaturePosition"
|
|
:signature-separator="signatureSeparator"
|
|
@update-signature="updateSignature"
|
|
/>
|
|
</FormSection>
|
|
<FormSection
|
|
:title="$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.TITLE')"
|
|
:description="$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.NOTE')"
|
|
>
|
|
<div
|
|
class="flex flex-col justify-between w-full gap-5 sm:gap-4 sm:flex-row"
|
|
>
|
|
<button
|
|
v-for="hotKey in hotKeys"
|
|
:key="hotKey.key"
|
|
class="px-0 reset-base w-full sm:flex-1 rounded-xl outline-1 outline"
|
|
:class="
|
|
isEditorHotKeyEnabled(hotKey.key)
|
|
? 'outline-n-brand/30'
|
|
: 'outline-n-weak'
|
|
"
|
|
>
|
|
<HotKeyCard
|
|
:key="hotKey.title"
|
|
:title="hotKey.title"
|
|
:description="hotKey.description"
|
|
:light-image="hotKey.lightImage"
|
|
:dark-image="hotKey.darkImage"
|
|
:active="isEditorHotKeyEnabled(hotKey.key)"
|
|
@click="toggleHotKey(hotKey.key)"
|
|
/>
|
|
</button>
|
|
</div>
|
|
</FormSection>
|
|
<FormSection
|
|
v-if="!globalConfig.disableUserProfileUpdate"
|
|
:title="$t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE')"
|
|
>
|
|
<ChangePassword />
|
|
</FormSection>
|
|
<FormSection
|
|
v-if="isMfaEnabled"
|
|
:title="$t('PROFILE_SETTINGS.FORM.SECURITY_SECTION.TITLE')"
|
|
:description="$t('PROFILE_SETTINGS.FORM.SECURITY_SECTION.NOTE')"
|
|
>
|
|
<MfaSettingsCard />
|
|
</FormSection>
|
|
<Policy :permissions="audioNotificationPermissions">
|
|
<FormSection
|
|
:title="$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE')"
|
|
:description="
|
|
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.NOTE')
|
|
"
|
|
>
|
|
<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 />
|
|
</FormSection>
|
|
</Policy>
|
|
<FormSection
|
|
:title="$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.TITLE')"
|
|
:description="
|
|
replaceInstallationName($t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.NOTE'))
|
|
"
|
|
>
|
|
<AccessToken
|
|
:value="currentUser.access_token"
|
|
@on-copy="onCopyToken"
|
|
@on-reset="resetAccessToken"
|
|
/>
|
|
</FormSection>
|
|
</div>
|
|
</template>
|