Merge branch 'develop' into fix/next-message-bubbles
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class SummaryReportsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('summary_reports', { accountScoped: true, apiVersion: 'v2' });
|
||||
}
|
||||
|
||||
getTeamReports({ since, until, businessHours } = {}) {
|
||||
return axios.get(`${this.url}/team`, {
|
||||
params: {
|
||||
since,
|
||||
until,
|
||||
business_hours: businessHours,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getAgentReports({ since, until, businessHours } = {}) {
|
||||
return axios.get(`${this.url}/agent`, {
|
||||
params: {
|
||||
since,
|
||||
until,
|
||||
business_hours: businessHours,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getInboxReports({ since, until, businessHours } = {}) {
|
||||
return axios.get(`${this.url}/inbox`, {
|
||||
params: {
|
||||
since,
|
||||
until,
|
||||
business_hours: businessHours,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new SummaryReportsAPI();
|
||||
@@ -8,6 +8,7 @@ import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import SidebarGroup from './SidebarGroup.vue';
|
||||
@@ -36,6 +37,18 @@ const toggleShortcutModalFn = show => {
|
||||
}
|
||||
};
|
||||
|
||||
const currentAccountId = useMapGetter('getCurrentAccountId');
|
||||
const isFeatureEnabledonAccount = useMapGetter(
|
||||
'accounts/isFeatureEnabledonAccount'
|
||||
);
|
||||
|
||||
const showV4Routes = computed(() => {
|
||||
return isFeatureEnabledonAccount.value(
|
||||
currentAccountId.value,
|
||||
FEATURE_FLAGS.REPORT_V4
|
||||
);
|
||||
});
|
||||
|
||||
useSidebarKeyboardShortcuts(toggleShortcutModalFn);
|
||||
|
||||
// We're using localStorage to store the expanded item in the sidebar
|
||||
@@ -77,6 +90,59 @@ const sortedInboxes = computed(() =>
|
||||
inboxes.value.slice().sort((a, b) => a.name.localeCompare(b.name))
|
||||
);
|
||||
|
||||
const newReportRoutes = [
|
||||
{
|
||||
name: 'Reports Agent',
|
||||
label: t('SIDEBAR.REPORTS_AGENT'),
|
||||
to: accountScopedRoute('agent_reports_index'),
|
||||
activeOn: ['agent_reports_show'],
|
||||
},
|
||||
{
|
||||
name: 'Reports Label',
|
||||
label: t('SIDEBAR.REPORTS_LABEL'),
|
||||
to: accountScopedRoute('label_reports'),
|
||||
},
|
||||
{
|
||||
name: 'Reports Inbox',
|
||||
label: t('SIDEBAR.REPORTS_INBOX'),
|
||||
to: accountScopedRoute('inbox_reports_index'),
|
||||
activeOn: ['inbox_reports_show'],
|
||||
},
|
||||
{
|
||||
name: 'Reports Team',
|
||||
label: t('SIDEBAR.REPORTS_TEAM'),
|
||||
to: accountScopedRoute('team_reports_index'),
|
||||
activeOn: ['team_reports_show'],
|
||||
},
|
||||
];
|
||||
|
||||
const oldReportRoutes = [
|
||||
{
|
||||
name: 'Reports Agent',
|
||||
label: t('SIDEBAR.REPORTS_AGENT'),
|
||||
to: accountScopedRoute('agent_reports'),
|
||||
},
|
||||
{
|
||||
name: 'Reports Label',
|
||||
label: t('SIDEBAR.REPORTS_LABEL'),
|
||||
to: accountScopedRoute('label_reports'),
|
||||
},
|
||||
{
|
||||
name: 'Reports Inbox',
|
||||
label: t('SIDEBAR.REPORTS_INBOX'),
|
||||
to: accountScopedRoute('inbox_reports'),
|
||||
},
|
||||
{
|
||||
name: 'Reports Team',
|
||||
label: t('SIDEBAR.REPORTS_TEAM'),
|
||||
to: accountScopedRoute('team_reports'),
|
||||
},
|
||||
];
|
||||
|
||||
const reportRoutes = computed(() =>
|
||||
showV4Routes.value ? newReportRoutes : oldReportRoutes
|
||||
);
|
||||
|
||||
const menuItems = computed(() => {
|
||||
return [
|
||||
{
|
||||
@@ -265,31 +331,12 @@ const menuItems = computed(() => {
|
||||
label: t('SIDEBAR.REPORTS_CONVERSATION'),
|
||||
to: accountScopedRoute('conversation_reports'),
|
||||
},
|
||||
...reportRoutes.value,
|
||||
{
|
||||
name: 'Reports CSAT',
|
||||
label: t('SIDEBAR.CSAT'),
|
||||
to: accountScopedRoute('csat_reports'),
|
||||
},
|
||||
{
|
||||
name: 'Reports Agent',
|
||||
label: t('SIDEBAR.REPORTS_AGENT'),
|
||||
to: accountScopedRoute('agent_reports'),
|
||||
},
|
||||
{
|
||||
name: 'Reports Label',
|
||||
label: t('SIDEBAR.REPORTS_LABEL'),
|
||||
to: accountScopedRoute('label_reports'),
|
||||
},
|
||||
{
|
||||
name: 'Reports Inbox',
|
||||
label: t('SIDEBAR.REPORTS_INBOX'),
|
||||
to: accountScopedRoute('inbox_reports'),
|
||||
},
|
||||
{
|
||||
name: 'Reports Team',
|
||||
label: t('SIDEBAR.REPORTS_TEAM'),
|
||||
to: accountScopedRoute('team_reports'),
|
||||
},
|
||||
{
|
||||
name: 'Reports SLA',
|
||||
label: t('SIDEBAR.REPORTS_SLA'),
|
||||
|
||||
@@ -11,6 +11,7 @@ const reports = accountId => ({
|
||||
'agent_reports',
|
||||
'label_reports',
|
||||
'inbox_reports',
|
||||
'inbox_reports_show',
|
||||
'team_reports',
|
||||
'sla_reports',
|
||||
],
|
||||
|
||||
@@ -40,7 +40,7 @@ const headerClass = computed(() =>
|
||||
:style="{
|
||||
width: `${header.getSize()}px`,
|
||||
}"
|
||||
class="text-left py-3 px-5 font-normal text-sm"
|
||||
class="text-left py-3 px-5 font-medium text-sm text-n-slate-12"
|
||||
:class="headerClass"
|
||||
@click="header.column.getCanSort() && header.column.toggleSorting()"
|
||||
>
|
||||
|
||||
@@ -37,8 +37,10 @@ const buttonStyleClass = props.compact
|
||||
>
|
||||
<Icon
|
||||
icon="i-lucide-chevron-left"
|
||||
class="size-5 ltr:-ml-1 rtl:-mr-1"
|
||||
:class="props.compact ? 'text-n-slate-11' : 'text-n-blue-text'"
|
||||
class="ltr:-ml-1 rtl:-mr-1"
|
||||
:class="
|
||||
props.compact ? 'text-n-slate-11 size-4' : 'text-n-blue-text size-5'
|
||||
"
|
||||
/>
|
||||
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
|
||||
</button>
|
||||
|
||||
@@ -33,4 +33,5 @@ export const FEATURE_FLAGS = {
|
||||
CAPTAIN: 'captain_integration',
|
||||
CUSTOM_ROLES: 'custom_roles',
|
||||
CHATWOOT_V4: 'chatwoot_v4',
|
||||
REPORT_V4: 'report_v4',
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CONVERSATION_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions';
|
||||
import { getUserPermissions } from 'dashboard/helper/permissionsHelper';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
class AudioNotificationStore {
|
||||
constructor(store) {
|
||||
@@ -18,6 +19,15 @@ class AudioNotificationStore {
|
||||
return mineConversation.some(conv => conv.unread_count > 0);
|
||||
};
|
||||
|
||||
isMessageFromPendingConversation = (message = {}) => {
|
||||
const { conversation_id: conversationId } = message || {};
|
||||
if (!conversationId) return false;
|
||||
|
||||
const activeConversation =
|
||||
this.store.getters.getConversationById(conversationId);
|
||||
return activeConversation?.status === wootConstants.STATUS_TYPE.PENDING;
|
||||
};
|
||||
|
||||
isMessageFromCurrentConversation = message => {
|
||||
return this.store.getters.getSelectedChat?.id === message.conversation_id;
|
||||
};
|
||||
|
||||
@@ -172,6 +172,12 @@ export class DashboardAudioNotificationHelper {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the conversation status is pending, then dismiss the alert
|
||||
// This case is common for all audio event types
|
||||
if (this.store.isMessageFromPendingConversation(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the message is sent by the current user then dismiss the alert
|
||||
if (isMessageFromCurrentUser(message, this.currentUser.id)) {
|
||||
return;
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
CONVERSATION_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions';
|
||||
import { getUserPermissions } from 'dashboard/helper/permissionsHelper';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
vi.mock('dashboard/helper/permissionsHelper', () => ({
|
||||
getUserPermissions: vi.fn(),
|
||||
}));
|
||||
@@ -18,6 +20,7 @@ describe('AudioNotificationStore', () => {
|
||||
getMineChats: vi.fn(),
|
||||
getSelectedChat: null,
|
||||
getCurrentAccountId: 1,
|
||||
getConversationById: vi.fn(),
|
||||
},
|
||||
};
|
||||
audioNotificationStore = new AudioNotificationStore(store);
|
||||
@@ -59,6 +62,63 @@ describe('AudioNotificationStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMessageFromPendingConversation', () => {
|
||||
it('should return true when conversation status is pending', () => {
|
||||
store.getters.getConversationById.mockReturnValue({
|
||||
id: 123,
|
||||
status: wootConstants.STATUS_TYPE.PENDING,
|
||||
});
|
||||
const message = { conversation_id: 123 };
|
||||
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromPendingConversation(message)
|
||||
).toBe(true);
|
||||
expect(store.getters.getConversationById).toHaveBeenCalledWith(123);
|
||||
});
|
||||
|
||||
it('should return false when conversation status is not pending', () => {
|
||||
store.getters.getConversationById.mockReturnValue({
|
||||
id: 123,
|
||||
status: wootConstants.STATUS_TYPE.OPEN,
|
||||
});
|
||||
const message = { conversation_id: 123 };
|
||||
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromPendingConversation(message)
|
||||
).toBe(false);
|
||||
expect(store.getters.getConversationById).toHaveBeenCalledWith(123);
|
||||
});
|
||||
|
||||
it('should return false when conversation is not found', () => {
|
||||
store.getters.getConversationById.mockReturnValue(null);
|
||||
const message = { conversation_id: 123 };
|
||||
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromPendingConversation(message)
|
||||
).toBe(false);
|
||||
expect(store.getters.getConversationById).toHaveBeenCalledWith(123);
|
||||
});
|
||||
|
||||
it('should return false when message has no conversation_id', () => {
|
||||
const message = {};
|
||||
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromPendingConversation(message)
|
||||
).toBe(false);
|
||||
expect(store.getters.getConversationById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false when message is null or undefined', () => {
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromPendingConversation(null)
|
||||
).toBe(false);
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromPendingConversation(undefined)
|
||||
).toBe(false);
|
||||
expect(store.getters.getConversationById).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMessageFromCurrentConversation', () => {
|
||||
it('should return true when message is from selected chat', () => {
|
||||
store.getters.getSelectedChat = { id: 6179 };
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
"HEADER": "Agents Overview",
|
||||
"DESCRIPTION": "Easily track agent performance with key metrics such as conversations, response times, resolution times, and resolved cases. Click an agent’s name to learn more.",
|
||||
"LOADING_CHART": "Loading chart data...",
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"DOWNLOAD_AGENT_REPORTS": "Download agent reports",
|
||||
@@ -258,6 +259,7 @@
|
||||
},
|
||||
"INBOX_REPORTS": {
|
||||
"HEADER": "Inbox Overview",
|
||||
"DESCRIPTION": "Quickly view your inbox performance with key metrics like conversations, response times, resolution times, and resolved cases—all in one place. Click an inbox name for more details.",
|
||||
"LOADING_CHART": "Loading chart data...",
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"DOWNLOAD_INBOX_REPORTS": "Download inbox reports",
|
||||
@@ -325,6 +327,7 @@
|
||||
},
|
||||
"TEAM_REPORTS": {
|
||||
"HEADER": "Team Overview",
|
||||
"DESCRIPTION": "Get a snapshot of your team’s performance with essential metrics, including conversations, response times, resolution times, and resolved cases. Click a team name for more details.",
|
||||
"LOADING_CHART": "Loading chart data...",
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"DOWNLOAD_TEAM_REPORTS": "Download team reports",
|
||||
@@ -538,5 +541,15 @@
|
||||
},
|
||||
"VIEW_DETAILS": "View Details"
|
||||
}
|
||||
},
|
||||
"SUMMARY_REPORTS": {
|
||||
"INBOX": "Inbox",
|
||||
"AGENT": "Agent",
|
||||
"TEAM": "Team",
|
||||
"AVG_RESOLUTION_TIME": "Avg. Resolution Time",
|
||||
"AVG_FIRST_RESPONSE_TIME": "Avg. First Response Time",
|
||||
"AVG_REPLY_TIME": "Avg. Customer Waiting Time",
|
||||
"RESOLUTION_COUNT": "Resolution Count",
|
||||
"CONVERSATIONS": "No. of conversations"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import ReportHeader from './components/ReportHeader.vue';
|
||||
import SummaryReports from './components/SummaryReports.vue';
|
||||
import V4Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const summarReportsRef = ref(null);
|
||||
|
||||
const onDownloadClick = () => {
|
||||
summarReportsRef.value.downloadReports();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ReportHeader
|
||||
:header-title="$t('AGENT_REPORTS.HEADER')"
|
||||
:header-description="$t('AGENT_REPORTS.DESCRIPTION')"
|
||||
>
|
||||
<V4Button
|
||||
:label="$t('AGENT_REPORTS.DOWNLOAD_AGENT_REPORTS')"
|
||||
icon="i-ph-download-simple"
|
||||
size="sm"
|
||||
@click="onDownloadClick"
|
||||
/>
|
||||
</ReportHeader>
|
||||
|
||||
<SummaryReports
|
||||
ref="summarReportsRef"
|
||||
action-key="summaryReports/fetchAgentSummaryReports"
|
||||
getter-key="agents/getAgents"
|
||||
fetch-items-key="agents/get"
|
||||
summary-key="summaryReports/getAgentSummaryReports"
|
||||
type="agent"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useFunctionGetter, useStore } from 'dashboard/composables/store';
|
||||
|
||||
import WootReports from './components/WootReports.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const agent = useFunctionGetter('agents/getAgentById', route.params.id);
|
||||
|
||||
onMounted(() => store.dispatch('agents/get'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WootReports
|
||||
v-if="agent.id"
|
||||
:key="agent.id"
|
||||
type="agent"
|
||||
getter-key="agents/getAgents"
|
||||
action-key="agents/get"
|
||||
:selected-item="agent"
|
||||
:download-button-label="$t('AGENT_REPORTS.DOWNLOAD_AGENT_REPORTS')"
|
||||
:report-title="$t('AGENT_REPORTS.HEADER')"
|
||||
has-back-button
|
||||
/>
|
||||
<div v-else class="w-full py-20">
|
||||
<Spinner class="mx-auto" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import ReportHeader from './components/ReportHeader.vue';
|
||||
import SummaryReports from './components/SummaryReports.vue';
|
||||
import V4Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const summarReportsRef = ref(null);
|
||||
|
||||
const onDownloadClick = () => {
|
||||
summarReportsRef.value.downloadReports();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ReportHeader
|
||||
:header-title="$t('INBOX_REPORTS.HEADER')"
|
||||
:header-description="$t('INBOX_REPORTS.DESCRIPTION')"
|
||||
>
|
||||
<V4Button
|
||||
:label="$t('INBOX_REPORTS.DOWNLOAD_INBOX_REPORTS')"
|
||||
icon="i-ph-download-simple"
|
||||
size="sm"
|
||||
@click="onDownloadClick"
|
||||
/>
|
||||
</ReportHeader>
|
||||
|
||||
<SummaryReports
|
||||
ref="summarReportsRef"
|
||||
action-key="summaryReports/fetchInboxSummaryReports"
|
||||
getter-key="inboxes/getInboxes"
|
||||
fetch-items-key="inboxes/get"
|
||||
summary-key="summaryReports/getInboxSummaryReports"
|
||||
type="inbox"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useFunctionGetter } from 'dashboard/composables/store';
|
||||
|
||||
import WootReports from './components/WootReports.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const inbox = useFunctionGetter('inboxes/getInboxById', route.params.id);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WootReports
|
||||
v-if="inbox.id"
|
||||
:key="inbox.id"
|
||||
type="inbox"
|
||||
getter-key="inboxes/getInboxes"
|
||||
action-key="inboxes/get"
|
||||
:selected-item="inbox"
|
||||
:download-button-label="$t('INBOX_REPORTS.DOWNLOAD_INBOX_REPORTS')"
|
||||
:report-title="$t('INBOX_REPORTS.HEADER')"
|
||||
has-back-button
|
||||
/>
|
||||
<div v-else class="w-full py-20">
|
||||
<Spinner class="mx-auto" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import ReportHeader from './components/ReportHeader.vue';
|
||||
import SummaryReports from './components/SummaryReports.vue';
|
||||
import V4Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const summarReportsRef = ref(null);
|
||||
|
||||
const onDownloadClick = () => {
|
||||
summarReportsRef.value.downloadReports();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ReportHeader
|
||||
:header-title="$t('TEAM_REPORTS.HEADER')"
|
||||
:header-description="$t('TEAM_REPORTS.DESCRIPTION')"
|
||||
>
|
||||
<V4Button
|
||||
:label="$t('TEAM_REPORTS.DOWNLOAD_TEAM_REPORTS')"
|
||||
icon="i-ph-download-simple"
|
||||
size="sm"
|
||||
@click="onDownloadClick"
|
||||
/>
|
||||
</ReportHeader>
|
||||
|
||||
<SummaryReports
|
||||
ref="summarReportsRef"
|
||||
action-key="summaryReports/fetchTeamSummaryReports"
|
||||
getter-key="teams/getTeams"
|
||||
fetch-items-key="teams/get"
|
||||
summary-key="summaryReports/getTeamSummaryReports"
|
||||
type="team"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useFunctionGetter } from 'dashboard/composables/store';
|
||||
|
||||
import WootReports from './components/WootReports.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const team = useFunctionGetter('teams/getTeamById', route.params.id);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WootReports
|
||||
v-if="team.id"
|
||||
:key="team.id"
|
||||
type="team"
|
||||
getter-key="teams/getTeams"
|
||||
action-key="teams/get"
|
||||
:selected-item="team"
|
||||
:download-button-label="$t('TEAM_REPORTS.DOWNLOAD_TEAM_REPORTS')"
|
||||
:report-title="$t('TEAM_REPORTS.HEADER')"
|
||||
has-back-button
|
||||
/>
|
||||
<div v-else class="w-full py-20">
|
||||
<Spinner class="mx-auto" />
|
||||
</div>
|
||||
</template>
|
||||
+8
-2
@@ -15,6 +15,10 @@ export default {
|
||||
Thumbnail,
|
||||
},
|
||||
props: {
|
||||
currentFilter: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
filterItemsList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
@@ -40,7 +44,7 @@ export default {
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
currentSelectedFilter: null,
|
||||
currentSelectedFilter: this.currentFilter || null,
|
||||
currentDateRangeSelection: {
|
||||
id: 0,
|
||||
name: this.$t('REPORT.DATE_RANGE_OPTIONS.LAST_7_DAYS'),
|
||||
@@ -113,7 +117,9 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
filterItemsList(val) {
|
||||
this.currentSelectedFilter = val[0];
|
||||
this.currentSelectedFilter = !this.currentFilter
|
||||
? val[0]
|
||||
: this.currentFilter;
|
||||
this.changeFilterSelection();
|
||||
},
|
||||
groupByFilterItemsList() {
|
||||
|
||||
+30
-6
@@ -1,17 +1,41 @@
|
||||
<script setup>
|
||||
import BackButton from 'dashboard/components/widgets/BackButton.vue';
|
||||
|
||||
defineProps({
|
||||
headerTitle: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
headerDescription: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasBackButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</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>
|
||||
<section class="flex flex-col gap-1 pt-10 pb-5">
|
||||
<div v-if="hasBackButton">
|
||||
<BackButton compact />
|
||||
</div>
|
||||
<div class="flex justify-between w-full gap-5">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>
|
||||
<span class="text-xl font-medium text-n-slate-12">
|
||||
{{ headerTitle }}
|
||||
</span>
|
||||
<p v-if="headerDescription" class="text-n-slate-12 mt-2">
|
||||
{{ headerDescription }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const routeName = computed(() => `${props.row.original.type}_reports_show`);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link
|
||||
:to="{ name: routeName, params: { id: row.original.id } }"
|
||||
class="text-n-slate-12 hover:underline"
|
||||
>
|
||||
{{ row.original.name }}
|
||||
</router-link>
|
||||
</template>
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
<script setup>
|
||||
import ReportFilterSelector from './FilterSelector.vue';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import Table from 'dashboard/components/table/Table.vue';
|
||||
import { generateFileName } from 'dashboard/helper/downloadHelper';
|
||||
import {
|
||||
useVueTable,
|
||||
createColumnHelper,
|
||||
getCoreRowModel,
|
||||
} from '@tanstack/vue-table';
|
||||
import { computed, onMounted, ref, h } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'account',
|
||||
},
|
||||
getterKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
actionKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
summaryKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
fetchItemsKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const from = ref(0);
|
||||
const to = ref(0);
|
||||
const businessHours = ref(false);
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import SummaryReportLink from './SummaryReportLink.vue';
|
||||
|
||||
const rowItems = useMapGetter([props.getterKey]) || [];
|
||||
const reportMetrics = useMapGetter([props.summaryKey]) || [];
|
||||
|
||||
const getMetrics = id =>
|
||||
reportMetrics.value.find(metrics => metrics.id === Number(id)) || {};
|
||||
const columnHelper = createColumnHelper();
|
||||
const { t } = useI18n();
|
||||
|
||||
const defaulSpanRender = cellProps =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: cellProps.getValue() ? '' : 'text-n-slate-12',
|
||||
},
|
||||
cellProps.getValue()
|
||||
);
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor('name', {
|
||||
header: t(`SUMMARY_REPORTS.${props.type.toUpperCase()}`),
|
||||
width: 300,
|
||||
cell: cellProps => h(SummaryReportLink, cellProps),
|
||||
}),
|
||||
columnHelper.accessor('conversationsCount', {
|
||||
header: t('SUMMARY_REPORTS.CONVERSATIONS'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('avgFirstResponseTime', {
|
||||
header: t('SUMMARY_REPORTS.AVG_FIRST_RESPONSE_TIME'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('avgResolutionTime', {
|
||||
header: t('SUMMARY_REPORTS.AVG_RESOLUTION_TIME'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('avgReplyTime', {
|
||||
header: t('SUMMARY_REPORTS.AVG_REPLY_TIME'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('resolutionsCount', {
|
||||
header: t('SUMMARY_REPORTS.RESOLUTION_COUNT'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
];
|
||||
|
||||
const renderAvgTime = value => (value ? formatTime(value) : '--');
|
||||
|
||||
const renderCount = value => (value ? value.toLocaleString() : '--');
|
||||
|
||||
const tableData = computed(() =>
|
||||
rowItems.value.map(row => {
|
||||
const rowMetrics = getMetrics(row.id);
|
||||
const {
|
||||
conversationsCount,
|
||||
avgFirstResponseTime,
|
||||
avgResolutionTime,
|
||||
avgReplyTime,
|
||||
resolvedConversationsCount,
|
||||
} = rowMetrics;
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
type: props.type,
|
||||
conversationsCount: renderCount(conversationsCount),
|
||||
avgFirstResponseTime: renderAvgTime(avgFirstResponseTime),
|
||||
avgReplyTime: renderAvgTime(avgReplyTime),
|
||||
avgResolutionTime: renderAvgTime(avgResolutionTime),
|
||||
resolutionsCount: renderCount(resolvedConversationsCount),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const fetchAllData = () => {
|
||||
store.dispatch(props.fetchItemsKey);
|
||||
store.dispatch(props.actionKey, {
|
||||
since: from.value,
|
||||
until: to.value,
|
||||
businessHours: businessHours.value,
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => fetchAllData());
|
||||
|
||||
const onFilterChange = updatedFilter => {
|
||||
from.value = updatedFilter.from;
|
||||
to.value = updatedFilter.to;
|
||||
businessHours.value = updatedFilter.businessHours;
|
||||
fetchAllData();
|
||||
};
|
||||
|
||||
const table = useVueTable({
|
||||
get data() {
|
||||
return tableData.value;
|
||||
},
|
||||
columns,
|
||||
enableSorting: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
// downloadReports method is not used in this component
|
||||
// but it is exposed to be used in the parent component
|
||||
const downloadReports = () => {
|
||||
const dispatchMethods = {
|
||||
agent: 'downloadAgentReports',
|
||||
label: 'downloadLabelReports',
|
||||
inbox: 'downloadInboxReports',
|
||||
team: 'downloadTeamReports',
|
||||
};
|
||||
if (dispatchMethods[props.type]) {
|
||||
const fileName = generateFileName({
|
||||
type: props.type,
|
||||
to: to.value,
|
||||
businessHours: businessHours.value,
|
||||
});
|
||||
const params = {
|
||||
from: from.value,
|
||||
to: to.value,
|
||||
fileName,
|
||||
businessHours: businessHours.value,
|
||||
};
|
||||
store.dispatch(dispatchMethods[props.type], params);
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ downloadReports });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ReportFilterSelector @filter-change="onFilterChange" />
|
||||
<div
|
||||
class="flex-1 overflow-auto px-5 py-6 mt-5 shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2"
|
||||
>
|
||||
<Table :table="table" />
|
||||
</div>
|
||||
</template>
|
||||
+11
-3
@@ -54,12 +54,20 @@ export default {
|
||||
type: String,
|
||||
default: 'Download Reports',
|
||||
},
|
||||
hasBackButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
selectedItem: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
from: 0,
|
||||
to: 0,
|
||||
selectedFilter: null,
|
||||
selectedFilter: this.selectedItem,
|
||||
groupBy: GROUP_BY_FILTER[1],
|
||||
groupByfilterItemsList: GROUP_BY_OPTIONS.DAY.map(this.translateOptions),
|
||||
selectedGroupByFilter: null,
|
||||
@@ -206,7 +214,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ReportHeader :header-title="reportTitle">
|
||||
<ReportHeader :header-title="reportTitle" :has-back-button="hasBackButton">
|
||||
<V4Button
|
||||
:label="downloadButtonLabel"
|
||||
icon="i-ph-download-simple"
|
||||
@@ -214,13 +222,13 @@ export default {
|
||||
@click="downloadReports"
|
||||
/>
|
||||
</ReportHeader>
|
||||
|
||||
<ReportFilters
|
||||
v-if="filterItemsList"
|
||||
:type="type"
|
||||
:filter-items-list="filterItemsList"
|
||||
:group-by-filter-items-list="groupByfilterItemsList"
|
||||
:selected-group-by-filter="selectedGroupByFilter"
|
||||
:current-filter="selectedFilter"
|
||||
@date-range-change="onDateRangeChange"
|
||||
@filter-change="onFilterChange"
|
||||
@group-by-filter-change="onGroupByFilterChange"
|
||||
|
||||
@@ -3,15 +3,112 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
import ReportsWrapper from './components/ReportsWrapper.vue';
|
||||
import Index from './Index.vue';
|
||||
|
||||
import AgentReportsIndex from './AgentReportsIndex.vue';
|
||||
import InboxReportsIndex from './InboxReportsIndex.vue';
|
||||
import TeamReportsIndex from './TeamReportsIndex.vue';
|
||||
|
||||
import AgentReportsShow from './AgentReportsShow.vue';
|
||||
import InboxReportsShow from './InboxReportsShow.vue';
|
||||
import TeamReportsShow from './TeamReportsShow.vue';
|
||||
|
||||
import AgentReports from './AgentReports.vue';
|
||||
import LabelReports from './LabelReports.vue';
|
||||
import InboxReports from './InboxReports.vue';
|
||||
import LabelReports from './LabelReports.vue';
|
||||
import TeamReports from './TeamReports.vue';
|
||||
|
||||
import CsatResponses from './CsatResponses.vue';
|
||||
import BotReports from './BotReports.vue';
|
||||
import LiveReports from './LiveReports.vue';
|
||||
import SLAReports from './SLAReports.vue';
|
||||
|
||||
const oldReportRoutes = [
|
||||
{
|
||||
path: 'agent',
|
||||
name: 'agent_reports',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: AgentReports,
|
||||
},
|
||||
{
|
||||
path: 'inboxes',
|
||||
name: 'inbox_reports',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: InboxReports,
|
||||
},
|
||||
{
|
||||
path: 'label',
|
||||
name: 'label_reports',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: LabelReports,
|
||||
},
|
||||
{
|
||||
path: 'teams',
|
||||
name: 'team_reports',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: TeamReports,
|
||||
},
|
||||
];
|
||||
|
||||
const revisedReportRoutes = [
|
||||
{
|
||||
path: 'agents_overview',
|
||||
name: 'agent_reports_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: AgentReportsIndex,
|
||||
},
|
||||
{
|
||||
path: 'agents/:id',
|
||||
name: 'agent_reports_show',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: AgentReportsShow,
|
||||
},
|
||||
|
||||
{
|
||||
path: 'inboxes_overview',
|
||||
name: 'inbox_reports_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: InboxReportsIndex,
|
||||
},
|
||||
{
|
||||
path: 'inboxes/:id',
|
||||
name: 'inbox_reports_show',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: InboxReportsShow,
|
||||
},
|
||||
{
|
||||
path: 'teams_overview',
|
||||
name: 'team_reports_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: TeamReportsIndex,
|
||||
},
|
||||
{
|
||||
path: 'teams/:id',
|
||||
name: 'team_reports_show',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: TeamReportsShow,
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
@@ -40,38 +137,8 @@ export default {
|
||||
},
|
||||
component: Index,
|
||||
},
|
||||
{
|
||||
path: 'agent',
|
||||
name: 'agent_reports',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: AgentReports,
|
||||
},
|
||||
{
|
||||
path: 'label',
|
||||
name: 'label_reports',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: LabelReports,
|
||||
},
|
||||
{
|
||||
path: 'inboxes',
|
||||
name: 'inbox_reports',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: InboxReports,
|
||||
},
|
||||
{
|
||||
path: 'teams',
|
||||
name: 'team_reports',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: TeamReports,
|
||||
},
|
||||
...oldReportRoutes,
|
||||
...revisedReportRoutes,
|
||||
{
|
||||
path: 'sla',
|
||||
name: 'sla_reports',
|
||||
|
||||
@@ -5,8 +5,8 @@ import agentBots from './modules/agentBots';
|
||||
import agents from './modules/agents';
|
||||
import articles from './modules/helpCenterArticles';
|
||||
import attributes from './modules/attributes';
|
||||
import auth from './modules/auth';
|
||||
import auditlogs from './modules/auditlogs';
|
||||
import auth from './modules/auth';
|
||||
import automations from './modules/automations';
|
||||
import bulkActions from './modules/bulkActions';
|
||||
import campaigns from './modules/campaigns';
|
||||
@@ -25,9 +25,10 @@ import conversationStats from './modules/conversationStats';
|
||||
import conversationTypingStatus from './modules/conversationTypingStatus';
|
||||
import conversationWatchers from './modules/conversationWatchers';
|
||||
import csat from './modules/csat';
|
||||
import customViews from './modules/customViews';
|
||||
import customRole from './modules/customRole';
|
||||
import customViews from './modules/customViews';
|
||||
import dashboardApps from './modules/dashboardApps';
|
||||
import draftMessages from './modules/draftMessages';
|
||||
import globalConfig from 'shared/store/globalConfig';
|
||||
import inboxAssignableAgents from './modules/inboxAssignableAgents';
|
||||
import inboxes from './modules/inboxes';
|
||||
@@ -39,12 +40,12 @@ import notifications from './modules/notifications';
|
||||
import portals from './modules/helpCenterPortals';
|
||||
import reports from './modules/reports';
|
||||
import sla from './modules/sla';
|
||||
import slaReports from './modules/SLAReports';
|
||||
import summaryReports from './modules/summaryReports';
|
||||
import teamMembers from './modules/teamMembers';
|
||||
import teams from './modules/teams';
|
||||
import userNotificationSettings from './modules/userNotificationSettings';
|
||||
import webhooks from './modules/webhooks';
|
||||
import draftMessages from './modules/draftMessages';
|
||||
import SLAReports from './modules/SLAReports';
|
||||
import captainAssistants from './captain/assistant';
|
||||
import captainDocuments from './captain/document';
|
||||
import captainResponses from './captain/response';
|
||||
@@ -58,9 +59,9 @@ export default createStore({
|
||||
agents,
|
||||
articles,
|
||||
attributes,
|
||||
auditlogs,
|
||||
auth,
|
||||
automations,
|
||||
auditlogs,
|
||||
bulkActions,
|
||||
campaigns,
|
||||
cannedResponse,
|
||||
@@ -78,9 +79,10 @@ export default createStore({
|
||||
conversationTypingStatus,
|
||||
conversationWatchers,
|
||||
csat,
|
||||
customViews,
|
||||
customRole,
|
||||
customViews,
|
||||
dashboardApps,
|
||||
draftMessages,
|
||||
globalConfig,
|
||||
inboxAssignableAgents,
|
||||
inboxes,
|
||||
@@ -91,13 +93,13 @@ export default createStore({
|
||||
notifications,
|
||||
portals,
|
||||
reports,
|
||||
sla,
|
||||
slaReports,
|
||||
summaryReports,
|
||||
teamMembers,
|
||||
teams,
|
||||
userNotificationSettings,
|
||||
webhooks,
|
||||
draftMessages,
|
||||
sla,
|
||||
slaReports: SLAReports,
|
||||
captainAssistants,
|
||||
captainDocuments,
|
||||
captainResponses,
|
||||
|
||||
@@ -22,6 +22,9 @@ export const getters = {
|
||||
getUIFlags($state) {
|
||||
return $state.uiFlags;
|
||||
},
|
||||
getAgentById: $state => id => {
|
||||
return $state.records.find(record => record.id === Number(id)) || {};
|
||||
},
|
||||
getAgentStatus($state) {
|
||||
let status = {
|
||||
online: $state.records.filter(
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import SummaryReportsAPI from 'dashboard/api/summaryReports';
|
||||
import store, { initialState } from '../summaryReports';
|
||||
|
||||
vi.mock('dashboard/api/summaryReports', () => ({
|
||||
default: {
|
||||
getInboxReports: vi.fn(),
|
||||
getAgentReports: vi.fn(),
|
||||
getTeamReports: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Summary Reports Store', () => {
|
||||
let commit;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks before each test
|
||||
vi.clearAllMocks();
|
||||
commit = vi.fn();
|
||||
});
|
||||
|
||||
describe('Initial State', () => {
|
||||
it('should have the correct initial state structure', () => {
|
||||
expect(initialState).toEqual({
|
||||
inboxSummaryReports: [],
|
||||
agentSummaryReports: [],
|
||||
teamSummaryReports: [],
|
||||
uiFlags: {
|
||||
isFetchingInboxSummaryReports: false,
|
||||
isFetchingAgentSummaryReports: false,
|
||||
isFetchingTeamSummaryReports: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Getters', () => {
|
||||
const state = {
|
||||
inboxSummaryReports: [{ id: 1 }],
|
||||
agentSummaryReports: [{ id: 2 }],
|
||||
teamSummaryReports: [{ id: 3 }],
|
||||
uiFlags: { isFetchingInboxSummaryReports: true },
|
||||
};
|
||||
|
||||
it('should return inbox summary reports', () => {
|
||||
expect(store.getters.getInboxSummaryReports(state)).toEqual([{ id: 1 }]);
|
||||
});
|
||||
|
||||
it('should return agent summary reports', () => {
|
||||
expect(store.getters.getAgentSummaryReports(state)).toEqual([{ id: 2 }]);
|
||||
});
|
||||
|
||||
it('should return team summary reports', () => {
|
||||
expect(store.getters.getTeamSummaryReports(state)).toEqual([{ id: 3 }]);
|
||||
});
|
||||
|
||||
it('should return UI flags', () => {
|
||||
expect(store.getters.getUIFlags(state)).toEqual({
|
||||
isFetchingInboxSummaryReports: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mutations', () => {
|
||||
it('should set inbox summary report', () => {
|
||||
const state = { ...initialState };
|
||||
const data = [{ id: 1 }];
|
||||
|
||||
store.mutations.setInboxSummaryReport(state, data);
|
||||
expect(state.inboxSummaryReports).toEqual(data);
|
||||
});
|
||||
|
||||
it('should set agent summary report', () => {
|
||||
const state = { ...initialState };
|
||||
const data = [{ id: 2 }];
|
||||
|
||||
store.mutations.setAgentSummaryReport(state, data);
|
||||
expect(state.agentSummaryReports).toEqual(data);
|
||||
});
|
||||
|
||||
it('should set team summary report', () => {
|
||||
const state = { ...initialState };
|
||||
const data = [{ id: 3 }];
|
||||
|
||||
store.mutations.setTeamSummaryReport(state, data);
|
||||
expect(state.teamSummaryReports).toEqual(data);
|
||||
});
|
||||
|
||||
it('should merge UI flags with existing flags', () => {
|
||||
const state = {
|
||||
uiFlags: { flag1: true, flag2: false },
|
||||
};
|
||||
const newFlags = { flag2: true, flag3: true };
|
||||
|
||||
store.mutations.setUIFlags(state, newFlags);
|
||||
expect(state.uiFlags).toEqual({
|
||||
flag1: true,
|
||||
flag2: true,
|
||||
flag3: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions', () => {
|
||||
describe('fetchInboxSummaryReports', () => {
|
||||
it('should fetch inbox reports successfully', async () => {
|
||||
const params = { date: '2025-01-01' };
|
||||
const mockResponse = {
|
||||
data: [{ report_id: 1, report_name: 'Test' }],
|
||||
};
|
||||
|
||||
SummaryReportsAPI.getInboxReports.mockResolvedValue(mockResponse);
|
||||
|
||||
await store.actions.fetchInboxSummaryReports({ commit }, params);
|
||||
|
||||
expect(commit).toHaveBeenCalledWith('setUIFlags', {
|
||||
isFetchingInboxSummaryReports: true,
|
||||
});
|
||||
expect(SummaryReportsAPI.getInboxReports).toHaveBeenCalledWith(params);
|
||||
expect(commit).toHaveBeenCalledWith('setInboxSummaryReport', [
|
||||
{ reportId: 1, reportName: 'Test' },
|
||||
]);
|
||||
expect(commit).toHaveBeenCalledWith('setUIFlags', {
|
||||
isFetchingInboxSummaryReports: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
SummaryReportsAPI.getInboxReports.mockRejectedValue(
|
||||
new Error('API Error')
|
||||
);
|
||||
|
||||
await store.actions.fetchInboxSummaryReports({ commit }, {});
|
||||
|
||||
expect(commit).toHaveBeenCalledWith('setUIFlags', {
|
||||
isFetchingInboxSummaryReports: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAgentSummaryReports', () => {
|
||||
it('should fetch agent reports successfully', async () => {
|
||||
const params = { agentId: 123 };
|
||||
const mockResponse = {
|
||||
data: [{ agent_id: 123, agent_name: 'Test Agent' }],
|
||||
};
|
||||
|
||||
SummaryReportsAPI.getAgentReports.mockResolvedValue(mockResponse);
|
||||
|
||||
await store.actions.fetchAgentSummaryReports({ commit }, params);
|
||||
|
||||
expect(commit).toHaveBeenCalledWith('setUIFlags', {
|
||||
isFetchingAgentSummaryReports: true,
|
||||
});
|
||||
expect(SummaryReportsAPI.getAgentReports).toHaveBeenCalledWith(params);
|
||||
expect(commit).toHaveBeenCalledWith('setAgentSummaryReport', [
|
||||
{ agentId: 123, agentName: 'Test Agent' },
|
||||
]);
|
||||
expect(commit).toHaveBeenCalledWith('setUIFlags', {
|
||||
isFetchingAgentSummaryReports: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchTeamSummaryReports', () => {
|
||||
it('should fetch team reports successfully', async () => {
|
||||
const params = { teamId: 456 };
|
||||
const mockResponse = {
|
||||
data: [{ team_id: 456, team_name: 'Test Team' }],
|
||||
};
|
||||
|
||||
SummaryReportsAPI.getTeamReports.mockResolvedValue(mockResponse);
|
||||
|
||||
await store.actions.fetchTeamSummaryReports({ commit }, params);
|
||||
|
||||
expect(commit).toHaveBeenCalledWith('setUIFlags', {
|
||||
isFetchingTeamSummaryReports: true,
|
||||
});
|
||||
expect(SummaryReportsAPI.getTeamReports).toHaveBeenCalledWith(params);
|
||||
expect(commit).toHaveBeenCalledWith('setTeamSummaryReport', [
|
||||
{ teamId: 456, teamName: 'Test Team' },
|
||||
]);
|
||||
expect(commit).toHaveBeenCalledWith('setUIFlags', {
|
||||
isFetchingTeamSummaryReports: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import SummaryReportsAPI from 'dashboard/api/summaryReports';
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
|
||||
const typeMap = {
|
||||
inbox: {
|
||||
flagKey: 'isFetchingInboxSummaryReports',
|
||||
apiMethod: 'getInboxReports',
|
||||
mutationKey: 'setInboxSummaryReport',
|
||||
},
|
||||
agent: {
|
||||
flagKey: 'isFetchingAgentSummaryReports',
|
||||
apiMethod: 'getAgentReports',
|
||||
mutationKey: 'setAgentSummaryReport',
|
||||
},
|
||||
team: {
|
||||
flagKey: 'isFetchingTeamSummaryReports',
|
||||
apiMethod: 'getTeamReports',
|
||||
mutationKey: 'setTeamSummaryReport',
|
||||
},
|
||||
};
|
||||
|
||||
async function fetchSummaryReports(type, params, { commit }) {
|
||||
const config = typeMap[type];
|
||||
if (!config) return;
|
||||
|
||||
try {
|
||||
commit('setUIFlags', { [config.flagKey]: true });
|
||||
const response = await SummaryReportsAPI[config.apiMethod](params);
|
||||
commit(config.mutationKey, camelcaseKeys(response.data, { deep: true }));
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
} finally {
|
||||
commit('setUIFlags', { [config.flagKey]: false });
|
||||
}
|
||||
}
|
||||
|
||||
export const initialState = {
|
||||
inboxSummaryReports: [],
|
||||
agentSummaryReports: [],
|
||||
teamSummaryReports: [],
|
||||
uiFlags: {
|
||||
isFetchingInboxSummaryReports: false,
|
||||
isFetchingAgentSummaryReports: false,
|
||||
isFetchingTeamSummaryReports: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getInboxSummaryReports(state) {
|
||||
return state.inboxSummaryReports;
|
||||
},
|
||||
getAgentSummaryReports(state) {
|
||||
return state.agentSummaryReports;
|
||||
},
|
||||
getTeamSummaryReports(state) {
|
||||
return state.teamSummaryReports;
|
||||
},
|
||||
getUIFlags(state) {
|
||||
return state.uiFlags;
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
fetchInboxSummaryReports({ commit }, params) {
|
||||
return fetchSummaryReports('inbox', params, { commit });
|
||||
},
|
||||
|
||||
fetchAgentSummaryReports({ commit }, params) {
|
||||
return fetchSummaryReports('agent', params, { commit });
|
||||
},
|
||||
|
||||
fetchTeamSummaryReports({ commit }, params) {
|
||||
return fetchSummaryReports('team', params, { commit });
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
setInboxSummaryReport(state, data) {
|
||||
state.inboxSummaryReports = data;
|
||||
},
|
||||
setAgentSummaryReport(state, data) {
|
||||
state.agentSummaryReports = data;
|
||||
},
|
||||
setTeamSummaryReport(state, data) {
|
||||
state.teamSummaryReports = data;
|
||||
},
|
||||
setUIFlags(state, uiFlag) {
|
||||
state.uiFlags = { ...state.uiFlags, ...uiFlag };
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: initialState,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
@@ -2,6 +2,12 @@ export const getters = {
|
||||
getTeams($state) {
|
||||
return Object.values($state.records).sort((a, b) => a.id - b.id);
|
||||
},
|
||||
getTeamById: $state => id => {
|
||||
return (
|
||||
Object.values($state.records).find(record => record.id === Number(id)) ||
|
||||
{}
|
||||
);
|
||||
},
|
||||
getMyTeams($state, $getters) {
|
||||
return $getters.getTeams.filter(team => {
|
||||
const { is_member: isMember } = team;
|
||||
|
||||
Reference in New Issue
Block a user