Merge branch 'develop' into design-updates

This commit is contained in:
Pranav
2024-12-05 09:57:22 -08:00
203 changed files with 10497 additions and 1303 deletions
@@ -83,7 +83,7 @@ export default {
this.filterTypes = [...this.filterTypes, ...filterTypes];
this.filterGroups = filterGroups;
if (this.getAppliedContactFilters.length) {
if (this.getAppliedContactFilters.length && !this.isSegmentsView) {
this.appliedFilters = [...this.getAppliedContactFilters];
} else if (!this.isSegmentsView) {
this.appliedFilters.push({
@@ -318,7 +318,7 @@ export default {
@reset-filter="resetFilter(i, appliedFilters[i])"
@remove-filter="removeFilter(i)"
/>
<div class="mt-4">
<div class="flex items-center gap-2 mt-4">
<woot-button
icon="add"
color-scheme="success"
@@ -171,7 +171,7 @@ const table = useVueTable({
<template>
<section class="flex-1 h-full overflow-auto bg-white dark:bg-slate-900">
<section class="overflow-x-auto">
<Table fixed :table="table" />
<Table fixed :table="table" type="compact" />
</section>
<EmptyState
@@ -1,120 +1,149 @@
<script>
import { mapGetters } from 'vuex';
import ContactInfoPanel from '../components/ContactInfoPanel.vue';
import ContactNotes from 'dashboard/modules/notes/NotesOnContactPage.vue';
import SettingsHeader from '../../settings/SettingsHeader.vue';
import Spinner from 'shared/components/Spinner.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
<script setup>
import { onMounted, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useRoute, useRouter } from 'vue-router';
export default {
components: {
ContactInfoPanel,
ContactNotes,
SettingsHeader,
Spinner,
Thumbnail,
},
props: {
contactId: {
type: [String, Number],
required: true,
},
},
data() {
return {
selectedTabIndex: 0,
};
},
computed: {
...mapGetters({
uiFlags: 'contacts/getUIFlags',
}),
tabs() {
return [
{
key: 0,
name: this.$t('NOTES.HEADER.TITLE'),
},
];
},
showEmptySearchResult() {
const hasEmptyResults = !!this.searchQuery && this.records.length === 0;
return hasEmptyResults;
},
contact() {
return this.$store.getters['contacts/getContact'](this.contactId);
},
backUrl() {
return `/app/accounts/${this.$route.params.accountId}/contacts`;
},
},
mounted() {
this.fetchContactDetails();
},
methods: {
onClickTabChange(index) {
this.selectedTabIndex = index;
},
fetchContactDetails() {
const { contactId: id } = this;
this.$store.dispatch('contacts/show', { id });
},
},
import ContactsDetailsLayout from 'dashboard/components-next/Contacts/ContactsDetailsLayout.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import ContactDetails from 'dashboard/components-next/Contacts/Pages/ContactDetails.vue';
import TabBar from 'dashboard/components-next/tabbar/TabBar.vue';
import ContactNotes from 'dashboard/components-next/Contacts/ContactsSidebar/ContactNotes.vue';
import ContactHistory from 'dashboard/components-next/Contacts/ContactsSidebar/ContactHistory.vue';
import ContactMerge from 'dashboard/components-next/Contacts/ContactsSidebar/ContactMerge.vue';
import ContactCustomAttributes from 'dashboard/components-next/Contacts/ContactsSidebar/ContactCustomAttributes.vue';
const store = useStore();
const route = useRoute();
const router = useRouter();
const contact = useMapGetter('contacts/getContactById');
const uiFlags = useMapGetter('contacts/getUIFlags');
const activeTab = ref('attributes');
const contactMergeRef = ref(null);
const isFetchingItem = computed(() => uiFlags.value.isFetchingItem);
const isMergingContact = computed(() => uiFlags.value.isMerging);
const selectedContact = computed(() => contact.value(route.params.contactId));
const showSpinner = computed(
() => isFetchingItem.value || isMergingContact.value
);
const { t } = useI18n();
const CONTACT_TABS_OPTIONS = [
{ key: 'ATTRIBUTES', value: 'attributes' },
{ key: 'HISTORY', value: 'history' },
{ key: 'NOTES', value: 'notes' },
{ key: 'MERGE', value: 'merge' },
];
const tabs = computed(() => {
return CONTACT_TABS_OPTIONS.map(tab => ({
label: t(`CONTACTS_LAYOUT.SIDEBAR.TABS.${tab.key}`),
value: tab.value,
}));
});
const activeTabIndex = computed(() => {
return CONTACT_TABS_OPTIONS.findIndex(v => v.value === activeTab.value);
});
const goToContactsList = () => {
if (window.history.state?.back || window.history.length > 1) {
router.back();
} else {
router.push(`/app/accounts/${route.params.accountId}/contacts?page=1`);
}
};
const fetchActiveContact = async () => {
if (route.params.contactId) {
store.dispatch('contacts/show', { id: route.params.contactId });
}
};
const handleTabChange = tab => {
activeTab.value = tab.value;
};
const fetchContactNotes = () => {
const { contactId } = route.params;
if (contactId) store.dispatch('contactNotes/get', { contactId });
};
const fetchContactConversations = () => {
const { contactId } = route.params;
if (contactId) store.dispatch('contactConversations/get', contactId);
};
const fetchAttributes = () => {
store.dispatch('attributes/get');
};
onMounted(() => {
fetchActiveContact();
fetchContactNotes();
fetchContactConversations();
fetchAttributes();
});
</script>
<template>
<div
class="flex justify-between flex-col h-full m-0 flex-1 bg-white dark:bg-slate-900"
class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-background"
>
<SettingsHeader
button-route="new"
:header-title="contact.name"
show-back-button
:back-button-label="$t('CONTACT_PROFILE.BACK_BUTTON')"
:back-url="backUrl"
:show-new-button="false"
<ContactsDetailsLayout
:button-label="$t('CONTACTS_LAYOUT.HEADER.MESSAGE_BUTTON')"
:selected-contact="selectedContact"
is-detail-view
:show-pagination-footer="false"
@go-to-contacts-list="goToContactsList"
>
<Thumbnail
v-if="contact.thumbnail"
:src="contact.thumbnail"
:username="contact.name"
size="32px"
class="mr-2 rtl:mr-0 rtl:ml-2"
/>
</SettingsHeader>
<div v-if="uiFlags.isFetchingItem" class="text-center p-4 text-base h-full">
<Spinner size="" />
<span>{{ $t('CONTACT_PROFILE.LOADING') }}</span>
</div>
<div v-else-if="contact.id" class="overflow-hidden flex-1 min-w-0">
<div class="flex flex-wrap ml-auto mr-auto max-w-full h-full">
<ContactInfoPanel
:show-close-button="false"
:show-avatar="false"
:contact="contact"
/>
<div class="w-3/4 h-full">
<woot-tabs :index="selectedTabIndex" @change="onClickTabChange">
<woot-tabs-item
v-for="(tab, index) in tabs"
:key="tab.key"
:index="index"
:name="tab.name"
:show-badge="false"
/>
</woot-tabs>
<div
class="bg-slate-25 dark:bg-slate-800 h-[calc(100%-40px)] p-4 overflow-auto"
>
<ContactNotes
v-if="selectedTabIndex === 0"
:contact-id="Number(contactId)"
/>
</div>
</div>
<div
v-if="showSpinner"
class="flex items-center justify-center py-10 text-n-slate-11"
>
<Spinner />
</div>
</div>
<ContactDetails
v-else-if="selectedContact"
:selected-contact="selectedContact"
@go-to-contacts-list="goToContactsList"
/>
<template #sidebar>
<div class="px-6">
<TabBar
:tabs="tabs"
:initial-active-tab="activeTabIndex"
class="w-full [&>button]:w-full bg-n-alpha-black2"
@tab-changed="handleTabChange"
/>
</div>
<div
v-if="isFetchingItem"
class="flex items-center justify-center py-10 text-n-slate-11"
>
<Spinner />
</div>
<template v-else>
<ContactCustomAttributes
v-if="activeTab === 'attributes'"
:selected-contact="selectedContact"
/>
<ContactNotes v-if="activeTab === 'notes'" />
<ContactHistory v-if="activeTab === 'history'" />
<ContactMerge
v-if="activeTab === 'merge'"
ref="contactMergeRef"
:selected-contact="selectedContact"
@go-to-contacts-list="goToContactsList"
/>
</template>
</template>
</ContactsDetailsLayout>
</div>
</template>
@@ -0,0 +1,300 @@
<script setup>
import { onMounted, computed, ref, reactive, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { debounce } from '@chatwoot/utils';
import { useUISettings } from 'dashboard/composables/useUISettings';
import filterQueryGenerator from 'dashboard/helper/filterQueryGenerator';
import ContactsListLayout from 'dashboard/components-next/Contacts/ContactsListLayout.vue';
import ContactsList from 'dashboard/components-next/Contacts/Pages/ContactsList.vue';
import ContactEmptyState from 'dashboard/components-next/Contacts/EmptyState/ContactEmptyState.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
const DEFAULT_SORT_FIELD = 'last_activity_at';
const DEBOUNCE_DELAY = 300;
const store = useStore();
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const { updateUISettings, uiSettings } = useUISettings();
const contacts = useMapGetter('contacts/getContactsList');
const uiFlags = useMapGetter('contacts/getUIFlags');
const customViewsUiFlags = useMapGetter('customViews/getUIFlags');
const segments = useMapGetter('customViews/getContactCustomViews');
const appliedFilters = useMapGetter('contacts/getAppliedContactFilters');
const meta = useMapGetter('contacts/getMeta');
const searchQuery = computed(() => route.query?.search);
const searchValue = ref(searchQuery.value || '');
const pageNumber = computed(() => Number(route.query?.page) || 1);
const parseSortSettings = (sortString = '') => {
const hasDescending = sortString.startsWith('-');
const sortField = hasDescending ? sortString.slice(1) : sortString;
return {
sort: sortField || DEFAULT_SORT_FIELD,
order: hasDescending ? '-' : '',
};
};
const { contacts_sort_by: contactSortBy = '' } = uiSettings.value ?? {};
const { sort: initialSort, order: initialOrder } =
parseSortSettings(contactSortBy);
const sortState = reactive({
activeSort: initialSort,
activeOrdering: initialOrder,
});
const activeLabel = computed(() => route.params.label);
const activeSegmentId = computed(() => route.params.segmentId);
const isFetchingList = computed(
() => uiFlags.value.isFetching || customViewsUiFlags.value.isFetching
);
const currentPage = computed(() => Number(meta.value?.currentPage));
const totalItems = computed(() => meta.value?.count);
const activeSegment = computed(() => {
if (!activeSegmentId.value) return undefined;
return segments.value.find(view => view.id === Number(activeSegmentId.value));
});
const hasContacts = computed(() => contacts.value.length > 0);
const isContactIndexView = computed(
() => route.name === 'contacts_dashboard_index' && pageNumber.value === 1
);
const hasAppliedFilters = computed(() => {
return appliedFilters.value.length > 0;
});
const showEmptyStateLayout = computed(() => {
return (
!searchQuery.value &&
!hasContacts.value &&
isContactIndexView.value &&
!hasAppliedFilters.value
);
});
const showEmptyText = computed(() => {
return (
(searchQuery.value ||
hasAppliedFilters.value ||
!isContactIndexView.value) &&
!hasContacts.value
);
});
const headerTitle = computed(() => {
if (searchQuery.value) return t('CONTACTS_LAYOUT.HEADER.SEARCH_TITLE');
if (activeSegmentId.value) return activeSegment.value?.name;
if (activeLabel.value) return `#${activeLabel.value}`;
return t('CONTACTS_LAYOUT.HEADER.TITLE');
});
const updatePageParam = (page, search = '') => {
const query = {
...route.query,
page: page.toString(),
...(search ? { search } : {}),
};
if (!search) {
delete query.search;
}
router.replace({ query });
};
const buildSortAttr = () =>
`${sortState.activeOrdering}${sortState.activeSort}`;
const getCommonFetchParams = (page = 1) => ({
page,
sortAttr: buildSortAttr(),
label: activeLabel.value,
});
const fetchContacts = async (page = 1) => {
await store.dispatch('contacts/clearContactFilters');
await store.dispatch('contacts/get', getCommonFetchParams(page));
updatePageParam(page);
};
const fetchSavedOrAppliedFilteredContact = async (payload, page = 1) => {
if (!activeSegmentId.value && !hasAppliedFilters.value) return;
await store.dispatch('contacts/filter', {
...getCommonFetchParams(page),
queryPayload: payload,
});
updatePageParam(page);
};
const searchContacts = debounce(async (value, page = 1) => {
await store.dispatch('contacts/clearContactFilters');
searchValue.value = value;
if (!value) {
updatePageParam(page);
await fetchContacts(page);
return;
}
updatePageParam(page, value);
await store.dispatch('contacts/search', {
...getCommonFetchParams(page),
search: encodeURIComponent(value),
});
}, DEBOUNCE_DELAY);
const fetchContactsBasedOnContext = async page => {
updatePageParam(page, searchValue.value);
if (isFetchingList.value) return;
if (searchQuery.value) {
await searchContacts(searchQuery.value, page);
return;
}
// Reset the search value when we change the view
searchValue.value = '';
// If there are applied filters or active segment with query
if (
(hasAppliedFilters.value || activeSegment.value?.query) &&
!activeLabel.value
) {
const queryPayload =
activeSegment.value?.query || filterQueryGenerator(appliedFilters.value);
await fetchSavedOrAppliedFilteredContact(queryPayload, page);
return;
}
// Default case: fetch regular contacts + label
await fetchContacts(page);
};
const handleSort = async ({ sort, order }) => {
Object.assign(sortState, { activeSort: sort, activeOrdering: order });
await updateUISettings({
contacts_sort_by: buildSortAttr(),
});
if (searchQuery.value) {
await searchContacts(searchValue.value);
return;
}
await (activeSegmentId.value || hasAppliedFilters.value
? fetchSavedOrAppliedFilteredContact(
activeSegmentId.value
? activeSegment.value?.query
: filterQueryGenerator(appliedFilters.value)
)
: fetchContacts());
};
const createContact = async contact => {
await store.dispatch('contacts/create', contact);
};
watch(
() => uiSettings.value?.contacts_sort_by,
newSortBy => {
if (newSortBy) {
const { sort, order } = parseSortSettings(newSortBy);
sortState.activeSort = sort;
sortState.activeOrdering = order;
}
},
{ immediate: true }
);
watch(
[activeLabel, activeSegment],
() => {
fetchContactsBasedOnContext(pageNumber.value);
},
{ deep: true }
);
watch(searchQuery, value => {
if (isFetchingList.value) return;
searchValue.value = value || '';
// Reset the view if there is search query when we click on the sidebar group
if (value === undefined) {
fetchContacts();
}
});
onMounted(async () => {
if (!activeSegmentId.value) {
if (searchQuery.value) {
await searchContacts(searchQuery.value, pageNumber.value);
return;
}
await fetchContacts(pageNumber.value);
} else if (activeSegment.value && activeSegmentId.value) {
await fetchSavedOrAppliedFilteredContact(
activeSegment.value.query,
pageNumber.value
);
}
});
</script>
<template>
<div
class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-background"
>
<ContactsListLayout
:search-value="searchValue"
:header-title="headerTitle"
:current-page="currentPage"
:total-items="totalItems"
:show-pagination-footer="!isFetchingList && hasContacts"
:active-sort="sortState.activeSort"
:active-ordering="sortState.activeOrdering"
:active-segment="activeSegment"
:segments-id="activeSegmentId"
:has-applied-filters="hasAppliedFilters"
@update:current-page="fetchContactsBasedOnContext"
@search="searchContacts"
@update:sort="handleSort"
@apply-filter="fetchSavedOrAppliedFilteredContact"
@clear-filters="fetchContacts"
>
<div
v-if="isFetchingList"
class="flex items-center justify-center py-10 text-n-slate-11"
>
<Spinner />
</div>
<template v-else>
<ContactEmptyState
v-if="showEmptyStateLayout"
class="pt-14"
:title="t('CONTACTS_LAYOUT.EMPTY_STATE.TITLE')"
:subtitle="t('CONTACTS_LAYOUT.EMPTY_STATE.SUBTITLE')"
:button-label="t('CONTACTS_LAYOUT.EMPTY_STATE.BUTTON_LABEL')"
@create="createContact"
/>
<div
v-else-if="showEmptyText"
class="flex items-center justify-center py-10"
>
<span class="text-base text-n-slate-11">
{{
searchQuery || !hasAppliedFilters
? t('CONTACTS_LAYOUT.EMPTY_STATE.SEARCH_EMPTY_STATE_TITLE')
: t('CONTACTS_LAYOUT.EMPTY_STATE.LIST_EMPTY_STATE_TITLE')
}}
</span>
</div>
<ContactsList v-else :contacts="contacts" />
</template>
</ContactsListLayout>
</div>
</template>
@@ -1,48 +1,60 @@
/* eslint arrow-body-style: 0 */
import { frontendURL } from '../../../helper/URLHelper';
import ContactsView from './components/ContactsView.vue';
import ContactsIndex from './pages/ContactsIndex.vue';
import ContactManageView from './pages/ContactManageView.vue';
const commonMeta = {
permissions: ['administrator', 'agent', 'contact_manage'],
};
export const routes = [
{
path: frontendURL('accounts/:accountId/contacts'),
name: 'contacts_dashboard',
meta: {
permissions: ['administrator', 'agent', 'contact_manage'],
},
component: ContactsView,
},
{
path: frontendURL('accounts/:accountId/contacts/custom_view/:id'),
name: 'contacts_segments_dashboard',
meta: {
permissions: ['administrator', 'agent', 'contact_manage'],
},
component: ContactsView,
props: route => {
return { segmentsId: route.params.id };
},
},
{
path: frontendURL('accounts/:accountId/labels/:label/contacts'),
name: 'contacts_labels_dashboard',
meta: {
permissions: ['administrator', 'agent', 'contact_manage'],
},
component: ContactsView,
props: route => {
return { label: route.params.label };
},
component: ContactsIndex,
meta: commonMeta,
children: [
{
path: '',
name: 'contacts_dashboard_index',
component: ContactsIndex,
meta: commonMeta,
},
{
path: 'segments/:segmentId',
name: 'contacts_dashboard_segments_index',
component: ContactsIndex,
meta: commonMeta,
},
{
path: 'labels/:label',
name: 'contacts_dashboard_labels_index',
component: ContactsIndex,
meta: commonMeta,
},
],
},
{
path: frontendURL('accounts/:accountId/contacts/:contactId'),
name: 'contact_profile_dashboard',
meta: {
permissions: ['administrator', 'agent', 'contact_manage'],
},
component: ContactManageView,
props: route => {
return { contactId: route.params.contactId };
},
meta: commonMeta,
children: [
{
path: '',
name: 'contacts_edit',
component: ContactManageView,
meta: commonMeta,
},
{
path: 'segments/:segmentId',
name: 'contacts_edit_segment',
component: ContactManageView,
meta: commonMeta,
},
{
path: 'labels/:label',
name: 'contacts_edit_label',
component: ContactManageView,
meta: commonMeta,
},
],
},
];
@@ -34,10 +34,11 @@ const portalLink = computed(() => {
);
});
const saveArticle = async ({ ...values }) => {
const saveArticle = async ({ ...values }, isAsync = false) => {
const actionToDispatch = isAsync ? 'articles/updateAsync' : 'articles/update';
isUpdating.value = true;
try {
await store.dispatch('articles/update', {
await store.dispatch(actionToDispatch, {
portalSlug,
articleId: articleSlug,
...values,
@@ -55,6 +56,10 @@ const saveArticle = async ({ ...values }) => {
}
};
const saveArticleAsync = async ({ ...values }) => {
saveArticle({ ...values }, true);
};
const isCategoryArticles = computed(() => {
return (
route.name === 'portals_categories_articles_index' ||
@@ -92,9 +97,7 @@ const previewArticle = () => {
});
};
onMounted(() => {
fetchArticleDetails();
});
onMounted(fetchArticleDetails);
</script>
<template>
@@ -103,6 +106,7 @@ onMounted(() => {
:is-updating="isUpdating"
:is-saved="isSaved"
@save-article="saveArticle"
@save-article-async="saveArticleAsync"
@preview-article="previewArticle"
@go-back="goBackToArticles"
/>
@@ -10,6 +10,7 @@ defineProps({
required: true,
},
});
const emit = defineEmits(['change']);
const onChange = (id, value) => {
emit('change', id, value);
@@ -23,18 +24,22 @@ const onChange = (id, value) => {
>
{{ label }}
</label>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-3 mt-2">
<div
v-for="item in items"
:key="item.id"
class="flex flex-row items-start gap-2"
>
<CheckBox
:id="`checkbox-condition-${item.value}`"
:is-checked="item.model"
:value="item.value"
@update="onChange"
/>
<label class="text-sm font-normal text-ash-900">
<label
class="text-sm font-normal text-ash-900"
:for="`checkbox-condition-${item.value}`"
>
{{ item.label }}
</label>
</div>
@@ -1,6 +1,7 @@
<script setup>
import { computed } from 'vue';
import { ALERT_EVENTS } from './constants';
import CheckBox from 'v3/components/Form/CheckBox.vue';
import { ALERT_EVENTS, EVENT_TYPES } from './constants';
const props = defineProps({
label: {
@@ -9,50 +10,83 @@ const props = defineProps({
},
value: {
type: String,
default: 'all',
default: '',
},
});
const emit = defineEmits(['update']);
const alertEvents = ALERT_EVENTS;
const alertEventValues = Object.values(EVENT_TYPES);
const selectedValue = computed({
get: () => props.value,
set: value => {
emit('update', value);
get: () => {
// maintain backward compatibility
if (props.value === 'none') return [];
if (props.value === 'mine') return [EVENT_TYPES.ASSIGNED];
if (props.value === 'all') return [...alertEventValues];
const validValues = props.value
.split('+')
.filter(value => alertEventValues.includes(value));
return [...new Set(validValues)];
},
set: value => {
const sortedValues = value.filter(Boolean).sort();
const uniqueValues = [...new Set(sortedValues)];
if (uniqueValues.length === 0) {
emit('update', 'none');
return;
}
emit('update', uniqueValues.join('+'));
},
});
const setValue = (isChecked, value) => {
let updatedValue = selectedValue.value;
if (isChecked) {
updatedValue.push(value);
} else {
updatedValue = updatedValue.filter(item => item !== value);
}
selectedValue.value = updatedValue;
};
const alertDescription = computed(() => {
const base =
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_COMBINATIONS.';
if (props.value === '' || props.value === 'none') {
return base + 'NONE';
}
return base + selectedValue.value.join('+').toUpperCase();
});
</script>
<template>
<div>
<label
class="flex justify-between pb-1 text-sm font-medium leading-6 text-ash-900"
>
<label class="pb-1 text-sm font-medium leading-6 text-ash-900">
{{ label }}
</label>
<div
class="flex flex-row justify-between h-10 max-w-xl p-2 border border-solid rounded-xl border-ash-200"
>
<div class="grid gap-3 mt-2">
<div
v-for="option in alertEvents"
:key="option.value"
class="flex flex-row items-center justify-center gap-2 px-4 border-r border-ash-200 grow last:border-r-0"
class="flex items-center gap-2"
>
<input
:id="`radio-${option.value}`"
v-model="selectedValue"
class="shadow-sm cursor-pointer grid place-items-center border-2 border-ash-200 appearance-none rounded-full w-4 h-4 checked:bg-primary-600 before:content-[''] before:bg-primary-600 before:border-4 before:rounded-full before:border-ash-25 checked:before:w-[14px] checked:before:h-[14px] checked:border checked:border-primary-600"
type="radio"
:value="option.value"
<CheckBox
:id="`checkbox-${option.value}`"
:is-checked="selectedValue.includes(option.value)"
@update="(_val, isChecked) => setValue(isChecked, option.value)"
/>
<label
:for="`radio-${option.value}`"
class="text-sm font-medium"
:class="
selectedValue === option.value ? 'text-ash-900' : 'text-ash-800'
"
:for="`checkbox-${option.value}`"
class="text-sm text-ash-900 font-normal"
>
{{
$t(
@@ -61,6 +95,9 @@ const selectedValue = computed({
}}
</label>
</div>
<div class="text-n-slate-11 text-sm font-medium mt-2">
{{ $t(alertDescription) }}
</div>
</div>
</div>
</template>
@@ -1,12 +1,15 @@
<script setup>
import { computed } from 'vue';
import Icon from 'next/icon/Icon.vue';
import * as Sentry from '@sentry/vue';
import FormSelect from 'v3/components/Form/Select.vue';
const props = defineProps({
value: {
type: String,
required: true,
validator: value => ['ding', 'bell'].includes(value),
validator: value =>
['ding', 'bell', 'chime', 'magic', 'ping'].includes(value),
},
label: {
type: String,
@@ -25,6 +28,18 @@ const alertTones = computed(() => [
value: 'bell',
label: 'Bell',
},
{
value: 'chime',
label: 'Chime',
},
{
value: 'magic',
label: 'Magic',
},
{
value: 'ping',
label: 'Ping',
},
]);
const selectedValue = computed({
@@ -33,25 +48,48 @@ const selectedValue = computed({
emit('change', value);
},
});
const audio = new Audio();
const playAudio = async () => {
try {
// Has great support https://caniuse.com/mdn-api_htmlaudioelement
audio.src = `/audio/dashboard/${selectedValue.value}.mp3`;
await audio.play();
} catch (error) {
Sentry.captureException(error);
}
};
</script>
<template>
<FormSelect
v-model="selectedValue"
name="alertTone"
spacing="compact"
:value="selectedValue"
:options="alertTones"
:label="label"
class="max-w-xl"
>
<option
v-for="tone in alertTones"
:key="tone.label"
:value="tone.value"
:selected="tone.value === selectedValue"
<div class="flex items-center gap-2">
<FormSelect
v-model="selectedValue"
name="alertTone"
spacing="compact"
class="flex-grow"
:value="selectedValue"
:options="alertTones"
:label="label"
>
{{ tone.label }}
</option>
</FormSelect>
<option
v-for="tone in alertTones"
:key="tone.label"
:value="tone.value"
:selected="tone.value === selectedValue"
>
{{ tone.label }}
</option>
</FormSelect>
<button
v-tooltip.top="
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.PLAY')
"
class="border-0 shadow-sm outline-none flex justify-center items-center size-10 appearance-none rounded-xl ring-ash-200 ring-1 ring-inset focus:ring-2 focus:ring-inset focus:ring-primary-500 flex-shrink-0 mt-[28px]"
@click="playAudio"
>
<Icon icon="i-lucide-volume-2" />
</button>
</div>
</template>
@@ -1,98 +1,91 @@
<script>
<script setup>
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import AudioAlertTone from './AudioAlertTone.vue';
import AudioAlertEvent from './AudioAlertEvent.vue';
import AudioAlertCondition from './AudioAlertCondition.vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useStore } from 'dashboard/composables/store';
const store = useStore();
import { useI18n } from 'vue-i18n';
import camelcaseKeys from 'camelcase-keys';
import { initializeAudioAlerts } from 'dashboard/helper/scriptHelpers';
import { useStoreGetters } from 'dashboard/composables/store';
export default {
components: {
AudioAlertEvent,
AudioAlertTone,
AudioAlertCondition,
},
setup() {
const { uiSettings, updateUISettings } = useUISettings();
const getters = useStoreGetters();
const currentUser = computed(() => getters.getCurrentUser.value);
return {
uiSettings,
updateUISettings,
};
},
data() {
return {
audioAlert: '',
playAudioWhenTabIsInactive: false,
alertIfUnreadConversationExist: false,
alertTone: 'ding',
audioAlertConditions: [],
};
},
watch: {
uiSettings(value) {
this.notificationUISettings(value);
},
},
mounted() {
this.notificationUISettings(this.uiSettings);
this.$store.dispatch('userNotificationSettings/get');
},
methods: {
notificationUISettings(uiSettings) {
const {
enable_audio_alerts: audioAlert = '',
always_play_audio_alert: alwaysPlayAudioAlert,
alert_if_unread_assigned_conversation_exist:
alertIfUnreadConversationExist,
notification_tone: alertTone,
} = uiSettings;
this.audioAlert = audioAlert;
this.playAudioWhenTabIsInactive = !alwaysPlayAudioAlert;
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
this.audioAlertConditions = [
{
id: 'audio1',
label: this.$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.CONDITION_ONE'
),
model: this.playAudioWhenTabIsInactive,
value: 'tab_is_inactive',
},
{
id: 'audio2',
label: this.$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.CONDITION_TWO'
),
model: this.alertIfUnreadConversationExist,
value: 'conversations_are_read',
},
];
this.alertTone = alertTone || 'ding';
},
handAudioAlertChange(value) {
this.audioAlert = value;
this.updateUISettings({
enable_audio_alerts: this.audioAlert,
});
useAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleAudioAlertConditions(id, value) {
if (id === 'tab_is_inactive') {
this.updateUISettings({
always_play_audio_alert: !value,
});
} else if (id === 'conversations_are_read') {
this.updateUISettings({
alert_if_unread_assigned_conversation_exist: value,
});
}
useAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleAudioToneChange(value) {
this.updateUISettings({ notification_tone: value });
useAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
const { uiSettings, updateUISettings } = useUISettings();
const { t } = useI18n();
const audioAlert = ref('');
const playAudioWhenTabIsInactive = ref(false);
const alertIfUnreadConversationExist = ref(false);
const alertTone = ref('ding');
const audioAlertConditions = ref([]);
const i18nKeyPrefix = 'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION';
const initializeNotificationUISettings = newUISettings => {
const updatedUISettings = camelcaseKeys(newUISettings);
audioAlert.value = updatedUISettings.enableAudioAlerts;
playAudioWhenTabIsInactive.value = !updatedUISettings.alwaysPlayAudioAlert;
alertIfUnreadConversationExist.value =
updatedUISettings.alertIfUnreadAssignedConversationExist;
audioAlertConditions.value = [
{
id: 'audio1',
label: t(`${i18nKeyPrefix}.CONDITIONS.CONDITION_ONE`),
model: playAudioWhenTabIsInactive.value,
value: 'tab_is_inactive',
},
{
id: 'audio2',
label: t(`${i18nKeyPrefix}.CONDITIONS.CONDITION_TWO`),
model: alertIfUnreadConversationExist.value,
value: 'conversations_are_read',
},
];
alertTone.value = updatedUISettings.notificationTone || 'ding';
};
watch(
uiSettings,
value => {
initializeNotificationUISettings(value);
},
{ immediate: true }
);
const handleAudioConfigChange = value => {
updateUISettings(value);
initializeAudioAlerts(currentUser.value);
useAlert(t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
};
onMounted(() => {
store.dispatch('userNotificationSettings/get');
});
const handAudioAlertChange = value => {
audioAlert.value = value;
handleAudioConfigChange({
enable_audio_alerts: value,
});
};
const handleAudioAlertConditions = (id, value) => {
if (id === 'tab_is_inactive') {
handleAudioConfigChange({
always_play_audio_alert: !value,
});
} else if (id === 'conversations_are_read') {
handleAudioConfigChange({
alert_if_unread_assigned_conversation_exist: value,
});
}
};
const handleAudioToneChange = value => {
handleAudioConfigChange({ notification_tone: value });
};
</script>
@@ -100,27 +93,19 @@ export default {
<div id="profile-settings-notifications" class="flex flex-col gap-6">
<AudioAlertTone
:value="alertTone"
:label="
$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.DEFAULT_TONE.TITLE'
)
"
:label="$t(`${i18nKeyPrefix}.DEFAULT_TONE.TITLE`)"
@change="handleAudioToneChange"
/>
<AudioAlertEvent
:label="
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPE.TITLE')
"
:label="$t(`${i18nKeyPrefix}.ALERT_TYPE.TITLE`)"
:value="audioAlert"
@update="handAudioAlertChange"
/>
<AudioAlertCondition
:items="audioAlertConditions"
:label="
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.TITLE')
"
:label="$t(`${i18nKeyPrefix}.CONDITIONS.TITLE`)"
@change="handleAudioAlertConditions"
/>
</div>
@@ -190,7 +190,6 @@ export default {
<UserProfilePicture
:src="avatarUrl"
:name="name"
size="72px"
@change="updateProfilePicture"
@delete="deleteProfilePicture"
/>
@@ -1,8 +1,6 @@
<script setup>
import { computed } from 'vue';
import ProfileAvatar from 'v3/components/Form/ProfileAvatar.vue';
import { removeEmoji } from 'shared/helpers/emoji';
const props = defineProps({
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
defineProps({
src: {
type: String,
default: '',
@@ -15,8 +13,6 @@ const props = defineProps({
const emit = defineEmits(['change', 'delete']);
const userNameWithoutEmoji = computed(() => removeEmoji(props.name));
const updateProfilePicture = e => {
emit('change', e);
};
@@ -31,10 +27,12 @@ const deleteProfilePicture = () => {
<span class="text-sm font-medium text-ash-900">
{{ $t('PROFILE_SETTINGS.FORM.PICTURE') }}
</span>
<ProfileAvatar
:src="src"
:name="userNameWithoutEmoji"
@change="updateProfilePicture"
<Avatar
:src="src || ''"
:name="name || ''"
:size="72"
allow-upload
@upload="updateProfilePicture"
@delete="deleteProfilePicture"
/>
</div>
@@ -36,17 +36,23 @@ export const NOTIFICATION_TYPES = [
},
];
export const EVENT_TYPES = {
ASSIGNED: 'assigned',
NOTME: 'notme',
UNASSIGNED: 'unassigned',
};
export const ALERT_EVENTS = [
{
value: 'none',
label: 'none',
value: EVENT_TYPES.ASSIGNED,
label: 'assigned',
},
{
value: 'mine',
label: 'mine',
value: EVENT_TYPES.UNASSIGNED,
label: 'unassigned',
},
{
value: 'all',
label: 'all',
value: EVENT_TYPES.NOTME,
label: 'notme',
},
];
@@ -1,11 +1,5 @@
<script>
<script setup>
import WootReports from './components/WootReports.vue';
export default {
components: {
WootReports,
},
};
</script>
<template>
@@ -15,5 +9,6 @@ export default {
getter-key="agents/getAgents"
action-key="agents/get"
:download-button-label="$t('REPORT.DOWNLOAD_AGENT_REPORTS')"
:report-title="$t('AGENT_REPORTS.HEADER')"
/>
</template>
@@ -5,11 +5,13 @@ import ReportFilterSelector from './components/FilterSelector.vue';
import { GROUP_BY_FILTER } from './constants';
import ReportContainer from './ReportContainer.vue';
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import ReportHeader from './components/ReportHeader.vue';
export default {
name: 'BotReports',
components: {
BotMetrics,
ReportHeader,
ReportFilterSelector,
ReportContainer,
},
@@ -84,21 +86,20 @@ export default {
</script>
<template>
<div class="flex-1 p-1 overflow-auto">
<div class="max-w-[960px] w-full mx-auto mb-10">
<ReportFilterSelector
:show-agents-filter="false"
show-group-by-filter
:show-business-hours-switch="false"
@filter-change="onFilterChange"
/>
<ReportHeader :header-title="$t('BOT_REPORTS.HEADER')" />
<div class="flex flex-col gap-4">
<ReportFilterSelector
:show-agents-filter="false"
show-group-by-filter
:show-business-hours-switch="false"
@filter-change="onFilterChange"
/>
<BotMetrics :filters="requestPayload" />
<ReportContainer
account-summary-key="getBotSummary"
:group-by="groupBy"
:report-keys="reportKeys"
/>
</div>
<BotMetrics :filters="requestPayload" />
<ReportContainer
account-summary-key="getBotSummary"
:group-by="groupBy"
:report-keys="reportKeys"
/>
</div>
</template>
@@ -7,6 +7,8 @@ import ReportFilterSelector from './components/FilterSelector.vue';
import { generateFileName } from '../../../../helper/downloadHelper';
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import { FEATURE_FLAGS } from '../../../../featureFlags';
import V4Button from 'dashboard/components-next/button/Button.vue';
import ReportHeader from './components/ReportHeader.vue';
export default {
name: 'CsatResponses',
@@ -14,6 +16,8 @@ export default {
CsatMetrics,
CsatTable,
ReportFilterSelector,
ReportHeader,
V4Button,
},
data() {
return {
@@ -108,26 +112,26 @@ export default {
</script>
<template>
<div class="flex-1 p-1 overflow-auto">
<div class="max-w-[960px] w-full mx-auto mb-10">
<ReportFilterSelector
show-agents-filter
show-inbox-filter
show-rating-filter
:show-team-filter="isTeamsEnabled"
:show-business-hours-switch="false"
@filter-change="onFilterChange"
/>
<woot-button
color-scheme="success"
class-names="button--fixed-top"
icon="arrow-download"
@click="downloadReports"
>
{{ $t('CSAT_REPORTS.DOWNLOAD') }}
</woot-button>
<CsatMetrics :filters="requestPayload" />
<CsatTable :page-index="pageIndex" @page-change="onPageNumberChange" />
</div>
<ReportHeader :header-title="$t('CSAT_REPORTS.HEADER')">
<V4Button
:label="$t('CSAT_REPORTS.DOWNLOAD')"
icon="i-ph-download-simple"
size="sm"
@click="downloadReports"
/>
</ReportHeader>
<div class="flex flex-col gap-4">
<ReportFilterSelector
show-agents-filter
show-inbox-filter
show-rating-filter
:show-team-filter="isTeamsEnabled"
:show-business-hours-switch="false"
@filter-change="onFilterChange"
/>
<CsatMetrics :filters="requestPayload" />
<CsatTable :page-index="pageIndex" @page-change="onPageNumberChange" />
</div>
</template>
@@ -1,11 +1,5 @@
<script>
<script setup>
import WootReports from './components/WootReports.vue';
export default {
components: {
WootReports,
},
};
</script>
<template>
@@ -15,5 +9,6 @@ export default {
getter-key="inboxes/getInboxes"
action-key="inboxes/get"
:download-button-label="$t('INBOX_REPORTS.DOWNLOAD_INBOX_REPORTS')"
:report-title="$t('INBOX_REPORTS.HEADER')"
/>
</template>
@@ -1,4 +1,5 @@
<script>
import V4Button from 'dashboard/components-next/button/Button.vue';
import { useAlert, useTrack } from 'dashboard/composables';
import fromUnixTime from 'date-fns/fromUnixTime';
import format from 'date-fns/format';
@@ -6,6 +7,7 @@ import ReportFilterSelector from './components/FilterSelector.vue';
import { GROUP_BY_FILTER } from './constants';
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import ReportContainer from './ReportContainer.vue';
import ReportHeader from './components/ReportHeader.vue';
const REPORTS_KEYS = {
CONVERSATIONS: 'conversations_count',
@@ -20,8 +22,10 @@ const REPORTS_KEYS = {
export default {
name: 'ConversationReports',
components: {
ReportHeader,
ReportFilterSelector,
ReportContainer,
V4Button,
},
data() {
return {
@@ -98,21 +102,20 @@ export default {
</script>
<template>
<div class="flex-1 p-1 overflow-auto">
<div class="max-w-[960px] w-full mx-auto mb-10">
<woot-button
color-scheme="primary"
icon="arrow-download"
@click="downloadAgentReports"
>
{{ $t('REPORT.DOWNLOAD_AGENT_REPORTS') }}
</woot-button>
<ReportFilterSelector
:show-agents-filter="false"
show-group-by-filter
@filter-change="onFilterChange"
/>
<ReportContainer :group-by="groupBy" />
</div>
<ReportHeader :header-title="$t('REPORT.HEADER')">
<V4Button
:label="$t('REPORT.DOWNLOAD_AGENT_REPORTS')"
icon="i-ph-download-simple"
size="sm"
@click="downloadAgentReports"
/>
</ReportHeader>
<div class="flex flex-col gap-3">
<ReportFilterSelector
:show-agents-filter="false"
show-group-by-filter
@filter-change="onFilterChange"
/>
<ReportContainer :group-by="groupBy" />
</div>
</template>
@@ -1,11 +1,5 @@
<script>
<script setup>
import WootReports from './components/WootReports.vue';
export default {
components: {
WootReports,
},
};
</script>
<template>
@@ -15,5 +9,6 @@ export default {
getter-key="labels/getLabels"
action-key="labels/get"
:download-button-label="$t('LABEL_REPORTS.DOWNLOAD_LABEL_REPORTS')"
:report-title="$t('LABEL_REPORTS.HEADER')"
/>
</template>
@@ -10,10 +10,12 @@ import getUnixTime from 'date-fns/getUnixTime';
import startOfDay from 'date-fns/startOfDay';
import subDays from 'date-fns/subDays';
import { emitter } from 'shared/helpers/mitt';
import ReportHeader from './components/ReportHeader.vue';
export default {
name: 'LiveReports',
components: {
ReportHeader,
AgentTable,
MetricCard,
ReportHeatmap,
@@ -123,83 +125,75 @@ export default {
</script>
<template>
<div class="flex-1 overflow-auto">
<div class="max-w-[960px] mx-auto w-full gap-3 flex flex-col p-1 pb-16">
<div class="flex flex-col items-center md:flex-row gap-3">
<div class="flex-1 w-full max-w-full md:w-[65%] md:max-w-[65%]">
<MetricCard
:header="$t('OVERVIEW_REPORTS.ACCOUNT_CONVERSATIONS.HEADER')"
:is-loading="uiFlags.isFetchingAccountConversationMetric"
:loading-message="$t('OVERVIEW_REPORTS.ACCOUNT_CONVERSATIONS.LOADING_MESSAGE')
"
>
<div class="grid grid-cols-2 auto-cols-auto xl:grid-cols-4 w-full">
<div
v-for="(metric, name, index) in conversationMetrics"
:key="index"
class="w-full"
>
<h3 class="text-base text-n-slate-11">
{{ name }}
</h3>
<p class="text-n-slate-12 text-3xl mb-0 mt-1">
{{ metric }}
</p>
</div>
</div>
</MetricCard>
</div>
<div class="flex-1 w-full max-w-full md:w-[35%] md:max-w-[35%]">
<MetricCard :header="$t('OVERVIEW_REPORTS.AGENT_STATUS.HEADER')">
<div class="grid grid-cols-2 auto-cols-auto xl:grid-cols-4 w-full">
<div
v-for="(metric, name, index) in agentStatusMetrics"
:key="index"
class="w-full"
>
<h3 class="text-base text-n-slate-11">
{{ name }}
</h3>
<p class="text-n-slate-12 text-3xl mb-0 mt-1">
{{ metric }}
</p>
</div>
</div>
</MetricCard>
</div>
</div>
<div class="flex flex-row w-full max-w-full ml-auto mr-auto">
<ReportHeader :header-title="$t('OVERVIEW_REPORTS.HEADER')" />
<div class="flex flex-col gap-4 pb-6">
<div class="flex flex-col items-center md:flex-row gap-4">
<div
class="flex-1 w-full max-w-full md:w-[65%] md:max-w-[65%] conversation-metric"
>
<MetricCard
:header="$t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.HEADER')"
>
<template #control>
<woot-button
icon="arrow-download"
size="small"
variant="smooth"
color-scheme="secondary"
@click="downloadHeatmapData"
>
{{ $t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.DOWNLOAD_REPORT') }}
</woot-button>
</template>
<ReportHeatmap
:heat-data="accountConversationHeatmap"
:is-loading="uiFlags.isFetchingAccountConversationsHeatmap"
/>
<div
v-for="(metric, name, index) in conversationMetrics"
:key="index"
class="flex-1 min-w-0 pb-2"
>
<h3 class="text-base text-n-slate-11">
{{ name }}
</h3>
<p class="text-n-slate-12 text-3xl mb-0 mt-1">
{{ metric }}
</p>
</div>
</MetricCard>
</div>
<div class="flex flex-row w-full max-w-full ml-auto mr-auto">
<MetricCard :header="$t('OVERVIEW_REPORTS.AGENT_CONVERSATIONS.HEADER')">
<AgentTable
:agents="agents"
:agent-metrics="agentConversationMetric"
:page-index="pageIndex"
:is-loading="uiFlags.isFetchingAgentConversationMetric"
@page-change="onPageNumberChange"
/>
<div class="flex-1 w-full max-w-full md:w-[35%] md:max-w-[35%]">
<MetricCard :header="$t('OVERVIEW_REPORTS.AGENT_STATUS.HEADER')">
<div
v-for="(metric, name, index) in agentStatusMetrics"
:key="index"
class="flex-1 min-w-0 pb-2"
>
<h3 class="text-base text-n-slate-11">
{{ name }}
</h3>
<p class="text-n-slate-12 text-3xl mb-0 mt-1">
{{ metric }}
</p>
</div>
</MetricCard>
</div>
</div>
<div class="flex flex-row flex-wrap max-w-full">
<MetricCard :header="$t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.HEADER')">
<template #control>
<woot-button
icon="arrow-download"
size="small"
variant="smooth"
color-scheme="secondary"
@click="downloadHeatmapData"
>
{{ $t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.DOWNLOAD_REPORT') }}
</woot-button>
</template>
<ReportHeatmap
:heat-data="accountConversationHeatmap"
:is-loading="uiFlags.isFetchingAccountConversationsHeatmap"
/>
</MetricCard>
</div>
<div class="flex flex-row flex-wrap max-w-full">
<MetricCard :header="$t('OVERVIEW_REPORTS.AGENT_CONVERSATIONS.HEADER')">
<AgentTable
:agents="agents"
:agent-metrics="agentConversationMetric"
:page-index="pageIndex"
:is-loading="uiFlags.isFetchingAgentConversationMetric"
@page-change="onPageNumberChange"
/>
</MetricCard>
</div>
</div>
</template>
@@ -135,7 +135,7 @@ export default {
<template>
<div
class="grid grid-cols-1 px-6 py-5 md:grid-cols-2 gap-5 shadow outline-1 outline outline-n-container group/cardLayout rounded-2xl bg-n-solid-2"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 px-6 py-5 shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2"
>
<div v-for="metric in metrics" :key="metric.KEY">
<ChartStats :metric="metric" :account-summary-key="accountSummaryKey" />
@@ -1,13 +1,17 @@
<script>
import V4Button from 'dashboard/components-next/button/Button.vue';
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import SLAMetrics from './components/SLA/SLAMetrics.vue';
import SLATable from './components/SLA/SLATable.vue';
import SLAReportFilters from './components/SLA/SLAReportFilters.vue';
import { generateFileName } from 'dashboard/helper/downloadHelper';
import ReportHeader from './components/ReportHeader.vue';
export default {
name: 'SLAReports',
components: {
V4Button,
ReportHeader,
SLAMetrics,
SLATable,
SLAReportFilters,
@@ -77,32 +81,28 @@ export default {
</script>
<template>
<div class="flex-1 p-1 overflow-auto">
<div class="max-w-[960px] w-full mx-auto mb-10 flex flex-col gap-6">
<SLAReportFilters @filter-change="onFilterChange" />
<woot-button
color-scheme="success"
class-names="button--fixed-top"
icon="arrow-download"
@click="downloadReports"
>
{{ $t('SLA_REPORTS.DOWNLOAD_SLA_REPORTS') }}
</woot-button>
<div class="flex flex-col gap-6">
<SLAMetrics
:hit-rate="slaMetrics.hitRate"
:no-of-breaches="slaMetrics.numberOfSLAMisses"
:no-of-conversations="slaMetrics.numberOfConversations"
:is-loading="uiFlags.isFetchingMetrics"
/>
<SLATable
:sla-reports="slaReports"
:is-loading="uiFlags.isFetching"
:current-page="Number(slaMeta.currentPage)"
:total-count="Number(slaMeta.count)"
@page-change="onPageChange"
/>
</div>
</div>
<ReportHeader :header-title="$t('SLA_REPORTS.HEADER')">
<V4Button
:label="$t('SLA_REPORTS.DOWNLOAD_SLA_REPORTS')"
icon="i-ph-download-simple"
size="sm"
@click="downloadReports"
/>
</ReportHeader>
<div class="flex flex-col flex-1 gap-6">
<SLAReportFilters @filter-change="onFilterChange" />
<SLAMetrics
:hit-rate="slaMetrics.hitRate"
:no-of-breaches="slaMetrics.numberOfSLAMisses"
:no-of-conversations="slaMetrics.numberOfConversations"
:is-loading="uiFlags.isFetchingMetrics"
/>
<SLATable
:sla-reports="slaReports"
:is-loading="uiFlags.isFetching"
:current-page="Number(slaMeta.currentPage)"
:total-count="Number(slaMeta.count)"
@page-change="onPageChange"
/>
</div>
</template>
@@ -1,11 +1,5 @@
<script>
<script setup>
import WootReports from './components/WootReports.vue';
export default {
components: {
WootReports,
},
};
</script>
<template>
@@ -15,5 +9,6 @@ export default {
getter-key="teams/getTeams"
action-key="teams/get"
:download-button-label="$t('TEAM_REPORTS.DOWNLOAD_TEAM_REPORTS')"
:report-title="$t('TEAM_REPORTS.HEADER')"
/>
</template>
@@ -38,7 +38,7 @@ onMounted(fetchMetrics);
<template>
<div
class="flex flex-wrap mx-0 bg-white dark:bg-slate-800 rounded-[4px] p-4 mb-5 border border-solid border-slate-75 dark:border-slate-700"
class="flex flex-wrap mx-0 shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2 px-6 py-5"
>
<ReportMetricCard
:label="$t('BOT_REPORTS.METRIC.TOTAL_CONVERSATIONS.LABEL')"
@@ -29,11 +29,11 @@ const trendColor = (value, key) => {
</script>
<template>
<div class="text-slate-900 dark:text-slate-100">
<div class="text-n-slate-11">
<span class="text-sm">
{{ metric.NAME }}
</span>
<div class="flex items-end">
<div class="flex items-end text-n-slate-12">
<div class="text-xl font-medium">
{{ displayMetric(metric.KEY) }}
</div>
@@ -86,7 +86,7 @@ export default {
<!-- Added ref for writing specs -->
<template>
<div
class="flex-col lg:flex-row flex flex-wrap mx-0 mb-5 shadow outline-1 outline outline-n-container group/cardLayout rounded-2xl bg-n-solid-2 px-6 py-5"
class="flex-col lg:flex-row flex flex-wrap mx-0 shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2 px-6 py-8 gap-4"
>
<CsatMetricCard
:label="$t('CSAT_REPORTS.METRIC.TOTAL_RESPONSES.LABEL')"
@@ -111,10 +111,10 @@ export default {
<div
v-if="metrics.totalResponseCount && !ratingFilterEnabled"
ref="csatBarChart"
class="w-full md:w-1/2 md:max-w-[50%] flex-1 rtl:[direction:initial] p-4"
class="w-full md:w-1/2 md:max-w-[50%] flex-1 rtl:[direction:initial]"
>
<h3
class="flex items-center m-0 text-xs font-medium md:text-sm text-slate-800 dark:text-slate-100"
class="flex items-center m-0 text-xs font-medium md:text-sm text-n-slate-12"
>
<div class="flex flex-row-reverse justify-end">
<div
@@ -146,12 +146,12 @@ const table = useVueTable({
<template>
<div
class="shadow outline-1 outline outline-n-container group/cardLayout rounded-2xl bg-n-solid-2 px-6 py-5"
class="shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2 px-6 py-5"
>
<Table :table="table" class="max-h-[calc(100vh-21.875rem)]" />
<div
v-show="!tableData.length"
class="flex items-center -mt-1 justify-center h-48 w-full border-0 text-slate-600 dark:text-slate-200"
class="h-48 flex items-center justify-center text-n-slate-12 text-sm"
>
{{ $t('CSAT_REPORTS.NO_RECORDS') }}
</div>
@@ -178,7 +178,7 @@ export default {
</script>
<template>
<div class="flex flex-col justify-between gap-3 mb-4 md:flex-row">
<div class="flex flex-col justify-between gap-3 md:flex-row">
<div
class="w-full grid gap-y-2 gap-x-1.5 grid-cols-[repeat(auto-fill,minmax(250px,1fr))]"
>
@@ -53,7 +53,6 @@ const closeDropdown = () => emit('closeDropdown');
<FilterButton
right-icon="chevron-down"
:button-text="name"
class="bg-slate-50 dark:bg-slate-800 hover:bg-slate-75 dark:hover:bg-slate-800"
@click="toggleDropdown"
>
<template v-if="showMenu && activeFilterType === type" #dropdown>
@@ -63,7 +63,7 @@ function getDayOfTheWeek(date) {
return days[dayIndex];
}
function getHeatmapLevelClass(value) {
if (!value) return 'outline-n-weak bg-n-solid-2';
if (!value) return 'outline-n-container dark:bg-slate-700/40 bg-slate-50/50';
let level = [...quantileRange.value, Infinity].findIndex(
range => value <= range && value > 0
@@ -72,7 +72,7 @@ function getHeatmapLevelClass(value) {
if (level > 6) level = 5;
if (level === 0) {
return 'outline-slate-100 dark:outline-slate-700 dark:bg-slate-700/40 bg-slate-50/50';
return 'outline-n-container dark:bg-slate-700/40 bg-slate-50/50';
}
const classes = [
@@ -0,0 +1,17 @@
<script setup>
defineProps({
headerTitle: {
required: true,
type: String,
},
});
</script>
<template>
<div class="flex items-center justify-between w-full h-20 gap-2">
<span class="text-xl font-medium text-n-slate-12">
{{ headerTitle }}
</span>
<slot />
</div>
</template>
@@ -22,26 +22,23 @@ defineProps({
<template>
<div
data-test-id="reportMetricContainer"
class="p-4 m-0"
:class="{
'grayscale pointer-events-none opacity-30': disabled,
}"
>
<h3
class="flex items-center m-0 text-sm font-medium text-slate-800 dark:text-slate-100"
>
<h3 class="flex items-center m-0 text-sm font-medium text-n-slate-11">
<span data-test-id="reportMetricLabel">{{ label }}</span>
<fluent-icon
v-tooltip="infoText"
data-test-id="reportMetricInfo"
size="14"
icon="info"
class="text-slate-500 dark:text-slate-200 my-0 mx-1 mt-0.5"
class="text-n-slate-11 my-0 mx-1 mt-0.5"
/>
</h3>
<h4
data-test-id="reportMetricValue"
class="mt-1 mb-0 text-3xl font-thin text-slate-700 dark:text-slate-100"
class="mt-1 mb-0 text-2xl text-n-slate-12"
>
{{ value }}
</h4>
@@ -0,0 +1,85 @@
<template>
<div
class="reports--wrapper overflow-auto bg-n-background w-full px-8 xl:px-0"
>
<div class="max-w-[960px] mx-auto pb-12">
<router-view />
</div>
</div>
</template>
<style scoped lang="scss">
.reports--wrapper {
::v-deep {
.multiselect--disabled {
@apply opacity-50 border border-n-weak rounded-md cursor-not-allowed;
}
.multiselect__content-wrapper {
@apply bg-n-solid-2 border border-n-weak text-n-slate-12;
}
.multiselect__tags {
@apply bg-n-slate-1 border border-n-weak m-0 min-h-[2.875rem] pt-0;
input[type='text'] {
@apply bg-n-alpha-3 border-n-weak !min-h-[2.375rem] !h-[2.375rem] !ps-0.5 !py-0 !text-sm;
}
}
.multiselect__placeholder {
@apply text-n-slate-11;
}
.multiselect__select {
@apply min-h-0;
}
.multiselect__single {
@apply bg-n-alpha-3 text-n-slate-11;
}
.multiselect__input {
@apply text-sm !h-[2.375rem] mb-0 !py-0;
}
.multiselect__tags,
.multiselect__input,
.multiselect {
@apply bg-n-alpha-3 !border-n-weak text-n-slate-12 rounded-lg text-sm min-h-[2.5rem];
}
.mx-input-wrapper {
@apply bg-n-alpha-3 !border-n-weak text-n-slate-12 rounded-lg text-sm;
input {
@apply border-n-weak text-sm;
}
}
.multiselect__option {
@apply flex items-center;
}
.mx-datepicker {
.mx-input {
@apply bg-n-alpha-3;
}
.mx-input-wrapper input::placeholder {
@apply text-n-slate-11;
}
.mx-input-wrapper input {
@apply text-n-slate-11;
}
}
.multiselect--active:not(.multiselect--above) .multiselect__current,
.multiselect--active:not(.multiselect--above) .multiselect__input,
.multiselect--active:not(.multiselect--above) .multiselect__tags {
@apply rounded-b-none;
}
}
}
</style>
@@ -24,7 +24,7 @@ export default {
<template>
<div class="flex flex-col gap-2 items-start justify-center min-w-[10rem]">
<span
class="inline-flex items-center gap-1 text-sm font-medium text-slate-700 dark:text-slate-200"
class="inline-flex items-center gap-1 text-sm font-medium text-n-slate-11"
>
{{ label }}
<fluent-icon
@@ -32,15 +32,15 @@ export default {
size="14"
icon="information"
type="outline"
class="flex flex-shrink-0 text-sm font-normal sm:font-medium text-slate-500 dark:text-slate-500"
class="flex flex-shrink-0 text-sm font-normal sm:font-medium text-n-slate-10"
/>
</span>
<div
v-if="isLoading"
class="w-12 h-6 mb-0.5 rounded-md bg-slate-50 dark:bg-slate-800 animate-pulse"
class="w-12 h-6 mb-0.5 rounded-md bg-n-slate-3 animate-pulse"
/>
<span v-else class="text-2xl font-medium text-slate-900 dark:text-slate-25">
<span v-else class="text-2xl font-medium text-n-slate-12">
{{ value }}
</span>
</div>
@@ -22,7 +22,7 @@ defineProps({
<template>
<div
class="flex sm:flex-row flex-col w-full gap-4 sm:gap-14 shadow outline-1 outline outline-n-container group/cardLayout rounded-2xl bg-n-solid-2 px-6 py-5"
class="flex sm:flex-row flex-col w-full gap-4 sm:gap-14 shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2 px-6 py-5"
>
<SLAMetricCard
:label="$t('SLA_REPORTS.METRICS.HIT_RATE.LABEL')"
@@ -31,18 +31,14 @@ defineProps({
:is-loading="isLoading"
/>
<div
class="w-full sm:w-px h-full border border-slate-75 dark:border-slate-700/50"
/>
<div class="w-full sm:w-px bg-n-strong" />
<SLAMetricCard
:label="$t('SLA_REPORTS.METRICS.NO_OF_MISSES.LABEL')"
:value="noOfBreaches"
:tool-tip="$t('SLA_REPORTS.METRICS.NO_OF_MISSES.TOOLTIP')"
:is-loading="isLoading"
/>
<div
class="w-full sm:w-px h-full border border-slate-75 dark:border-slate-700/50"
/>
<div class="w-full sm:w-px bg-n-strong" />
<SLAMetricCard
:label="$t('SLA_REPORTS.METRICS.NO_OF_CONVERSATIONS.LABEL')"
:value="noOfConversations"
@@ -31,22 +31,23 @@ const conversationLabels = computed(() => {
<template>
<div
class="grid items-center content-center w-full h-16 grid-cols-12 gap-4 px-6 py-0 bg-white border-b last:border-b-0 last:rounded-b-xl border-slate-75 dark:border-slate-800/50 dark:bg-slate-900"
class="grid items-center content-center w-full h-16 grid-cols-12 gap-4 px-6 py-0 border-b last:border-b-0 last:rounded-b-xl border-n-weak"
>
<div
class="flex items-center gap-2 col-span-6 px-0 py-2 text-sm tracking-[0.5] text-slate-700 dark:text-slate-100 rtl:text-right"
>
<span class="text-slate-700 dark:text-slate-200">
<span class="text-n-slate-12">
{{ `#${conversationId} ` }}
</span>
<span class="text-slate-600 dark:text-slate-300">
<span class="text-slate-11">
{{ $t('SLA_REPORTS.WITH') }}
</span>
<span class="capitalize truncate text-slate-700 dark:text-slate-200">{{
<span class="capitalize truncate text-n-slate-12">{{
conversation.contact.name
}}</span>
<CardLabels
class="w-[80%]"
v-if="conversationLabels.length"
class="w-[60%]"
:conversation-id="conversationId"
:conversation-labels="conversationLabels"
/>
@@ -61,7 +62,7 @@ const conversationLabels = computed(() => {
v-if="conversation.assignee"
:user="conversation.assignee"
/>
<span v-else class="text-slate-600 dark:text-slate-200"> --- </span>
<span v-else class="text-n-slate-11"> --- </span>
</div>
<SLAViewDetails :sla-events="slaEvents" />
</div>
@@ -57,9 +57,11 @@ export default {
<template>
<div>
<div
class="min-w-full shadow outline-1 outline outline-n-container group/cardLayout rounded-2xl bg-n-solid-2"
class="min-w-full shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2 p-6"
>
<div class="grid content-center h-12 grid-cols-12 gap-4 px-6 py-0">
<div
class="grid content-center h-12 grid-cols-12 gap-4 px-6 py-0 bg-n-slate-2 rounded-md"
>
<TableHeaderCell
:span="6"
:label="$t('SLA_REPORTS.TABLE.HEADER.CONVERSATION')"
@@ -72,7 +74,7 @@ export default {
:span="2"
:label="$t('SLA_REPORTS.TABLE.HEADER.AGENT')"
/>
<TableHeaderCell :span="2" label="" />
<TableHeaderCell :span="1" label="" />
</div>
<div v-if="isLoading" class="flex items-center justify-center h-32">
@@ -89,10 +91,7 @@ export default {
:sla-events="slaReport.sla_events"
/>
</div>
<div
v-else
class="flex items-center justify-center h-32 bg-white rounded-b-xl dark:bg-slate-900"
>
<div v-else class="flex items-center justify-center h-32">
{{ $t('SLA_REPORTS.NO_RECORDS') }}
</div>
</div>
@@ -29,24 +29,23 @@ export default {
</script>
<template>
<div v-on-clickaway="closeSlaEvents" class="label-wrap">
<div
class="flex items-center col-span-2 px-0 py-2 text-sm tracking-[0.5] text-slate-700 dark:text-slate-100 rtl:text-right"
>
<div class="relative">
<woot-button
color-scheme="secondary"
variant="link"
@click="openSlaEvents"
>
{{ $t('SLA_REPORTS.TABLE.VIEW_DETAILS') }}
</woot-button>
<SLAPopoverCard
v-if="showSlaPopoverCard"
:sla-missed-events="slaEvents"
class="right-0"
/>
</div>
<div
v-on-clickaway="closeSlaEvents"
class="flex items-center col-span-2 text-slate-11 justify-end"
>
<div class="relative">
<woot-button
color-scheme="secondary"
variant="link"
@click="openSlaEvents"
>
{{ $t('SLA_REPORTS.TABLE.VIEW_DETAILS') }}
</woot-button>
<SLAPopoverCard
v-if="showSlaPopoverCard"
:sla-missed-events="slaEvents"
class="right-0"
/>
</div>
</div>
</template>
@@ -1,10 +1,12 @@
<script>
import V4Button from 'dashboard/components-next/button/Button.vue';
import { useAlert, useTrack } from 'dashboard/composables';
import ReportFilters from './ReportFilters.vue';
import ReportContainer from '../ReportContainer.vue';
import { GROUP_BY_FILTER } from '../constants';
import { generateFileName } from '../../../../../helper/downloadHelper';
import { REPORTS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
import ReportHeader from './ReportHeader.vue';
const GROUP_BY_OPTIONS = {
DAY: [{ id: 1, groupByKey: 'REPORT.GROUPING_OPTIONS.DAY' }],
@@ -26,6 +28,8 @@ const GROUP_BY_OPTIONS = {
export default {
components: {
ReportHeader,
V4Button,
ReportFilters,
ReportContainer,
},
@@ -46,6 +50,10 @@ export default {
type: String,
default: 'Download Reports',
},
reportTitle: {
type: String,
default: 'Download Reports',
},
},
data() {
return {
@@ -198,32 +206,29 @@ export default {
</script>
<template>
<div class="flex-1 p-1 overflow-auto">
<div class="max-w-[960px] w-full mx-auto mb-10">
<woot-button
color-scheme="success"
class-names="button--fixed-top"
icon="arrow-download"
@click="downloadReports"
>
{{ downloadButtonLabel }}
</woot-button>
<ReportFilters
v-if="filterItemsList"
:type="type"
:filter-items-list="filterItemsList"
:group-by-filter-items-list="groupByfilterItemsList"
:selected-group-by-filter="selectedGroupByFilter"
@date-range-change="onDateRangeChange"
@filter-change="onFilterChange"
@group-by-filter-change="onGroupByFilterChange"
@business-hours-toggle="onBusinessHoursToggle"
/>
<ReportContainer
v-if="filterItemsList.length"
:group-by="groupBy"
:report-keys="reportKeys"
/>
</div>
</div>
<ReportHeader :header-title="reportTitle">
<V4Button
:label="downloadButtonLabel"
icon="i-ph-download-simple"
size="sm"
@click="downloadReports"
/>
</ReportHeader>
<ReportFilters
v-if="filterItemsList"
:type="type"
:filter-items-list="filterItemsList"
:group-by-filter-items-list="groupByfilterItemsList"
:selected-group-by-filter="selectedGroupByFilter"
@date-range-change="onDateRangeChange"
@filter-change="onFilterChange"
@group-by-filter-change="onGroupByFilterChange"
@business-hours-toggle="onBusinessHoursToggle"
/>
<ReportContainer
v-if="filterItemsList.length"
:group-by="groupBy"
:report-keys="reportKeys"
/>
</template>
@@ -144,10 +144,7 @@ const table = useVueTable({
<template>
<div class="agent-table-container">
<Table
:table="table"
class="max-h-[calc(100vh-21.875rem)] border border-slate-50 dark:border-slate-800"
/>
<Table :table="table" class="max-h-[calc(100vh-21.875rem)]" />
<Pagination class="mt-2" :table="table" />
<div v-if="isLoading" class="agents-loader">
<Spinner />
@@ -169,7 +166,7 @@ const table = useVueTable({
.ve-table {
&::v-deep {
th.ve-table-header-th {
font-size: var(--font-size-mini) !important;
@apply text-sm rounded-xl;
padding: var(--space-small) var(--space-two) !important;
}
@@ -25,25 +25,23 @@ export default {
<template>
<div
class="metric-card flex flex-col px-6 py-5 overflow-hidden flex-grow text-slate-700 dark:text-slate-100 min-h-[8rem] shadow outline-1 outline outline-n-container group/cardLayout rounded-2xl bg-n-solid-2"
class="flex flex-col m-0.5 px-6 py-5 overflow-hidden rounded-xl flex-grow text-n-slate-12 shadow outline-1 outline outline-n-container bg-n-solid-2 min-h-[10rem]"
>
<div
class="card-header grid w-full mb-4 grid-cols-[repeat(auto-fit,minmax(max-content,50%))] gap-y-2"
>
<slot name="header">
<div class="flex items-center gap-0.5 flex-row">
<h5
class="mb-0 text-slate-800 dark:text-slate-100 font-medium text-lg"
>
<div class="flex items-center gap-2 flex-row">
<h5 class="mb-0 text-n-slate-12 font-medium text-lg">
{{ header }}
</h5>
<span
class="flex flex-row items-center pr-2 pl-2 m-1 rounded-sm text-green-400 dark:text-green-400 text-xs bg-green-100/30 dark:bg-green-100/20"
class="flex flex-row items-center py-0.5 px-2 rounded bg-n-teal-3 text-xs"
>
<span
class="bg-green-500 dark:bg-green-500 h-1 w-1 rounded-full mr-1 rtl:mr-0 rtl:ml-0"
class="bg-n-teal-9 h-1 w-1 rounded-full mr-1 rtl:mr-0 rtl:ml-0"
/>
<span>
<span class="text-xs text-n-teal-11">
{{ $t('OVERVIEW_REPORTS.LIVE') }}
</span>
</span>
@@ -66,7 +64,7 @@ export default {
class="items-center flex text-base justify-center px-12 py-6"
>
<Spinner />
<span class="text-slate-300 dark:text-slate-200">
<span class="text-n-slate-11">
{{ loadingMessage }}
</span>
</div>
@@ -1,7 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`CsatMetrics.vue > computes response count correctly 1`] = `
"<div class="flex-col lg:flex-row flex flex-wrap mx-0 bg-white dark:bg-slate-800 rounded-[4px] p-4 mb-5 border border-solid border-slate-75 dark:border-slate-700">
"<div class="flex-col lg:flex-row flex flex-wrap mx-0 shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2 px-6 py-8 gap-4">
<csat-metric-card-stub label="CSAT_REPORTS.METRIC.TOTAL_RESPONSES.LABEL" infotext="CSAT_REPORTS.METRIC.TOTAL_RESPONSES.TOOLTIP" disabled="false" class="xs:w-full sm:max-w-[50%] lg:w-1/6 lg:max-w-[16%]" value="100"></csat-metric-card-stub>
<csat-metric-card-stub label="CSAT_REPORTS.METRIC.SATISFACTION_SCORE.LABEL" infotext="CSAT_REPORTS.METRIC.SATISFACTION_SCORE.TOOLTIP" disabled="true" class="xs:w-full sm:max-w-[50%] lg:w-1/6 lg:max-w-[16%]" value="--"></csat-metric-card-stub>
<csat-metric-card-stub label="CSAT_REPORTS.METRIC.RESPONSE_RATE.LABEL" infotext="CSAT_REPORTS.METRIC.RESPONSE_RATE.TOOLTIP" disabled="false" class="xs:w-full sm:max-w-[50%] lg:w-1/6 lg:max-w-[16%]" value="90%"></csat-metric-card-stub>
@@ -1,7 +1,7 @@
import { frontendURL } from '../../../../helper/URLHelper';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import SettingsContent from '../Wrapper.vue';
import ReportsWrapper from './components/ReportsWrapper.vue';
import Index from './Index.vue';
import AgentReports from './AgentReports.vue';
import LabelReports from './LabelReports.vue';
@@ -16,11 +16,7 @@ export default {
routes: [
{
path: frontendURL('accounts/:accountId/reports'),
component: SettingsContent,
props: {
headerTitle: 'OVERVIEW_REPORTS.HEADER',
keepAlive: false,
},
component: ReportsWrapper,
children: [
{
path: '',
@@ -36,16 +32,6 @@ export default {
},
component: LiveReports,
},
],
},
{
path: frontendURL('accounts/:accountId/reports'),
component: SettingsContent,
props: {
headerTitle: 'REPORT.HEADER',
keepAlive: false,
},
children: [
{
path: 'conversation',
name: 'conversation_reports',
@@ -54,53 +40,6 @@ export default {
},
component: Index,
},
],
},
{
path: frontendURL('accounts/:accountId/reports'),
component: SettingsContent,
props: {
headerTitle: 'CSAT_REPORTS.HEADER',
keepAlive: false,
},
children: [
{
path: 'csat',
name: 'csat_reports',
meta: {
permissions: ['administrator', 'report_manage'],
},
component: CsatResponses,
},
],
},
{
path: frontendURL('accounts/:accountId/reports'),
component: SettingsContent,
props: {
headerTitle: 'BOT_REPORTS.HEADER',
keepAlive: false,
},
children: [
{
path: 'bot',
name: 'bot_reports',
meta: {
permissions: ['administrator', 'report_manage'],
featureFlag: FEATURE_FLAGS.RESPONSE_BOT,
},
component: BotReports,
},
],
},
{
path: frontendURL('accounts/:accountId/reports'),
component: SettingsContent,
props: {
headerTitle: 'AGENT_REPORTS.HEADER',
keepAlive: false,
},
children: [
{
path: 'agent',
name: 'agent_reports',
@@ -109,16 +48,6 @@ export default {
},
component: AgentReports,
},
],
},
{
path: frontendURL('accounts/:accountId/reports'),
component: SettingsContent,
props: {
headerTitle: 'LABEL_REPORTS.HEADER',
keepAlive: false,
},
children: [
{
path: 'label',
name: 'label_reports',
@@ -127,16 +56,6 @@ export default {
},
component: LabelReports,
},
],
},
{
path: frontendURL('accounts/:accountId/reports'),
component: SettingsContent,
props: {
headerTitle: 'INBOX_REPORTS.HEADER',
keepAlive: false,
},
children: [
{
path: 'inboxes',
name: 'inbox_reports',
@@ -145,15 +64,6 @@ export default {
},
component: InboxReports,
},
],
},
{
path: frontendURL('accounts/:accountId/reports'),
component: SettingsContent,
props: {
headerTitle: 'TEAM_REPORTS.HEADER',
},
children: [
{
path: 'teams',
name: 'team_reports',
@@ -162,16 +72,6 @@ export default {
},
component: TeamReports,
},
],
},
{
path: frontendURL('accounts/:accountId/reports'),
component: SettingsContent,
props: {
headerTitle: 'SLA_REPORTS.HEADER',
keepAlive: false,
},
children: [
{
path: 'sla',
name: 'sla_reports',
@@ -181,6 +81,22 @@ export default {
},
component: SLAReports,
},
{
path: 'csat',
name: 'csat_reports',
meta: {
permissions: ['administrator', 'report_manage'],
},
component: CsatResponses,
},
{
path: 'bot',
name: 'bot_reports',
meta: {
permissions: ['administrator', 'report_manage'],
},
component: BotReports,
},
],
},
],