Merge branch 'develop' into design-updates
This commit is contained in:
+2
-2
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
+9
-5
@@ -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>
|
||||
|
||||
+1
-1
@@ -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')"
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+3
-3
@@ -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>
|
||||
|
||||
+1
-1
@@ -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))]"
|
||||
>
|
||||
|
||||
-1
@@ -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 = [
|
||||
|
||||
+17
@@ -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>
|
||||
+3
-6
@@ -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>
|
||||
|
||||
+85
@@ -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>
|
||||
+4
-4
@@ -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>
|
||||
|
||||
+3
-7
@@ -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"
|
||||
|
||||
+7
-6
@@ -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>
|
||||
|
||||
+6
-7
@@ -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>
|
||||
|
||||
+17
-18
@@ -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>
|
||||
|
||||
+33
-28
@@ -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>
|
||||
|
||||
+2
-5
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+7
-9
@@ -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
-1
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user