feat: Add reports about live agent load (#4537)
* feat: Add reports about live agent load
This commit is contained in:
parent
899176a793
commit
676796ddc7
28 changed files with 758 additions and 48 deletions
|
@ -4,6 +4,7 @@ class V2::ReportBuilder
|
|||
attr_reader :account, :params
|
||||
|
||||
DEFAULT_GROUP_BY = 'day'.freeze
|
||||
AGENT_RESULTS_PER_PAGE = 10
|
||||
|
||||
def initialize(account, params)
|
||||
@account = account
|
||||
|
@ -79,12 +80,15 @@ class V2::ReportBuilder
|
|||
end
|
||||
|
||||
def agent_metrics
|
||||
users = @account.users
|
||||
users = users.where(id: params[:user_id]) if params[:user_id].present?
|
||||
users.each_with_object([]) do |user, arr|
|
||||
@user = user
|
||||
account_users = @account.account_users.page(params[:page]).per(AGENT_RESULTS_PER_PAGE)
|
||||
account_users.each_with_object([]) do |account_user, arr|
|
||||
@user = account_user.user
|
||||
arr << {
|
||||
user: { id: user.id, name: user.name, thumbnail: user.avatar_url },
|
||||
id: @user.id,
|
||||
name: @user.name,
|
||||
email: @user.email,
|
||||
thumbnail: @user.avatar_url,
|
||||
availability: account_user.availability_status,
|
||||
metric: conversations
|
||||
}
|
||||
end
|
||||
|
@ -94,7 +98,7 @@ class V2::ReportBuilder
|
|||
@open_conversations = scope.conversations.open
|
||||
first_response_count = scope.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
|
||||
metric = {
|
||||
open: @open_conversations.count,
|
||||
total: @open_conversations.count,
|
||||
unattended: @open_conversations.count - first_response_count
|
||||
}
|
||||
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
|
||||
|
|
|
@ -82,7 +82,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
|||
def conversation_params
|
||||
{
|
||||
type: params[:type].to_sym,
|
||||
user_id: params[:user_id]
|
||||
user_id: params[:user_id],
|
||||
page: params[:page].presence || 1
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -44,6 +44,15 @@ class ReportsAPI extends ApiClient {
|
|||
});
|
||||
}
|
||||
|
||||
getConversationMetric(type = 'account', page = 1) {
|
||||
return axios.get(`${this.url}/conversations`, {
|
||||
params: {
|
||||
type,
|
||||
page,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getAgentReports(since, until) {
|
||||
return axios.get(`${this.url}/agents`, {
|
||||
params: { since, until },
|
||||
|
|
|
@ -97,5 +97,18 @@ describe('#Reports API', () => {
|
|||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#getConversationMetric', () => {
|
||||
reportsAPI.getConversationMetric('account');
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/conversations',
|
||||
{
|
||||
params: {
|
||||
type: 'account',
|
||||
page: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,8 @@ import { frontendURL } from '../../../../helper/URLHelper';
|
|||
const reports = accountId => ({
|
||||
parentNav: 'reports',
|
||||
routes: [
|
||||
'settings_account_reports',
|
||||
'account_overview_reports',
|
||||
'conversation_reports',
|
||||
'csat_reports',
|
||||
'agent_reports',
|
||||
'label_reports',
|
||||
|
@ -16,7 +17,14 @@ const reports = accountId => ({
|
|||
label: 'REPORTS_OVERVIEW',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/overview`),
|
||||
toStateName: 'settings_account_reports',
|
||||
toStateName: 'account_overview_reports',
|
||||
},
|
||||
{
|
||||
icon: 'chat',
|
||||
label: 'REPORTS_CONVERSATION',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/conversation`),
|
||||
toStateName: 'conversation_reports',
|
||||
},
|
||||
{
|
||||
icon: 'emoji',
|
||||
|
|
|
@ -23,6 +23,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
'contact.updated': this.onContactUpdate,
|
||||
'conversation.mentioned': this.onConversationMentioned,
|
||||
'notification.created': this.onNotificationCreated,
|
||||
'first.reply.created': this.onFirstReplyCreated,
|
||||
'conversation.read': this.onConversationRead,
|
||||
};
|
||||
}
|
||||
|
@ -128,6 +129,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
|
||||
fetchConversationStats = () => {
|
||||
bus.$emit('fetch_conversation_stats');
|
||||
bus.$emit('fetch_overview_reports');
|
||||
};
|
||||
|
||||
onContactDelete = data => {
|
||||
|
@ -145,6 +147,10 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
onNotificationCreated = data => {
|
||||
this.app.$store.dispatch('notifications/addNotification', data);
|
||||
};
|
||||
|
||||
onFirstReplyCreated = () => {
|
||||
bus.$emit('fetch_overview_reports');
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
|
@ -108,6 +108,7 @@
|
|||
"GO_TO_CONVERSATION_DASHBOARD": "Go to Conversation Dashboard",
|
||||
"GO_TO_CONTACTS_DASHBOARD": "Go to Contacts Dashboard",
|
||||
"GO_TO_REPORTS_OVERVIEW": "Go to Reports Overview",
|
||||
"GO_TO_CONVERSATION_REPORTS": "Go to Conversation Reports",
|
||||
"GO_TO_AGENT_REPORTS": "Go to Agent Reports",
|
||||
"GO_TO_LABEL_REPORTS": "Go to Label Reports",
|
||||
"GO_TO_INBOX_REPORTS": "Go to Inbox Reports",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"REPORT": {
|
||||
"HEADER": "Overview",
|
||||
"HEADER": "Conversations",
|
||||
"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",
|
||||
|
@ -381,5 +381,33 @@
|
|||
"TOOLTIP": "Total number of responses / Total number of CSAT survey messages sent * 100"
|
||||
}
|
||||
}
|
||||
},
|
||||
"OVERVIEW_REPORTS": {
|
||||
"HEADER": "Overview",
|
||||
"LIVE": "Live",
|
||||
"ACCOUNT_CONVERSATIONS": {
|
||||
"HEADER": "Open Conversations",
|
||||
"LOADING_MESSAGE": "Conversations Loading...",
|
||||
"TOTAL" : "Total",
|
||||
"UNATTENDED": "Unattended",
|
||||
"UNASSIGNED": "Unassigned"
|
||||
},
|
||||
"AGENT_CONVERSATIONS": {
|
||||
"HEADER": "Conversations by agents",
|
||||
"LOADING_MESSAGE": "Agents Loading...",
|
||||
"NO_AGENTS": "There are no conversations by agents",
|
||||
"TABLE_HEADER": {
|
||||
"AGENT": "Agent",
|
||||
"TOTAL": "Total",
|
||||
"UNATTENDED": "Unattended",
|
||||
"STATUS": "Status"
|
||||
}
|
||||
},
|
||||
"AGENT_STATUS": {
|
||||
"HEADER": "Agent status",
|
||||
"ONLINE": "Online",
|
||||
"BUSY": "Busy",
|
||||
"OFFLINE": "Offline"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"TITLE": "Personal message signature",
|
||||
"NOTE": "Create a personal message signature that would be added to all the messages you send from the platform. Use the rich content editor to create a highly personalised signature.",
|
||||
"BTN_TEXT": "Save message signature",
|
||||
"API_ERROR":"Couldn't save signature! Try again",
|
||||
"API_ERROR": "Couldn't save signature! Try again",
|
||||
"API_SUCCESS": "Signature saved successfully"
|
||||
},
|
||||
"MESSAGE_SIGNATURE": {
|
||||
|
@ -173,7 +173,7 @@
|
|||
"NEW_LABEL": "New label",
|
||||
"NEW_TEAM": "New team",
|
||||
"NEW_INBOX": "New inbox",
|
||||
"REPORTS_OVERVIEW": "Overview",
|
||||
"REPORTS_CONVERSATION": "Conversations",
|
||||
"CSAT": "CSAT",
|
||||
"CAMPAIGNS": "Campaigns",
|
||||
"ONGOING": "Ongoing",
|
||||
|
@ -183,7 +183,8 @@
|
|||
"REPORTS_INBOX": "Inbox",
|
||||
"REPORTS_TEAM": "Team",
|
||||
"SET_AVAILABILITY_TITLE": "Set yourself as",
|
||||
"BETA": "Beta"
|
||||
"BETA": "Beta",
|
||||
"REPORTS_OVERVIEW": "Overview"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
|
||||
|
|
|
@ -13,6 +13,7 @@ export const ICON_SNOOZE_UNTIL_TOMORRROW = `<svg role="img" class="ninja-icon ni
|
|||
export const ICON_CONVERSATION_DASHBOARD = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M10.55 2.532a2.25 2.25 0 0 1 2.9 0l6.75 5.692c.507.428.8 1.057.8 1.72v9.803a1.75 1.75 0 0 1-1.75 1.75h-3.5a1.75 1.75 0 0 1-1.75-1.75v-5.5a.25.25 0 0 0-.25-.25h-3.5a.25.25 0 0 0-.25.25v5.5a1.75 1.75 0 0 1-1.75 1.75h-3.5A1.75 1.75 0 0 1 3 19.747V9.944c0-.663.293-1.292.8-1.72l6.75-5.692zm1.933 1.147a.75.75 0 0 0-.966 0L4.767 9.37a.75.75 0 0 0-.267.573v9.803c0 .138.112.25.25.25h3.5a.25.25 0 0 0 .25-.25v-5.5c0-.967.784-1.75 1.75-1.75h3.5c.966 0 1.75.783 1.75 1.75v5.5c0 .138.112.25.25.25h3.5a.25.25 0 0 0 .25-.25V9.944a.75.75 0 0 0-.267-.573l-6.75-5.692z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_CONTACT_DASHBOARD = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M17.754 14a2.249 2.249 0 0 1 2.25 2.249v.575c0 .894-.32 1.76-.902 2.438c-1.57 1.834-3.957 2.739-7.102 2.739c-3.146 0-5.532-.905-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.577a2.249 2.249 0 0 1 2.249-2.25h11.501zm0 1.5H6.253a.749.749 0 0 0-.75.749v.577c0 .536.192 1.054.54 1.461c1.253 1.468 3.219 2.214 5.957 2.214s4.706-.746 5.962-2.214a2.25 2.25 0 0 0 .541-1.463v-.575a.749.749 0 0 0-.749-.75zM12 2.004a5 5 0 1 1 0 10a5 5 0 0 1 0-10zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_REPORTS_OVERVIEW = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M16.749 2h4.554l.1.014l.099.028l.06.026c.08.034.153.085.219.15l.04.044l.044.057l.054.09l.039.09l.019.064l.014.064l.009.095v4.532a.75.75 0 0 1-1.493.102l-.007-.102V4.559l-6.44 6.44a.75.75 0 0 1-.976.073L13 11L9.97 8.09l-5.69 5.689a.75.75 0 0 1-1.133-.977l.073-.084l6.22-6.22a.75.75 0 0 1 .976-.072l.084.072l3.03 2.91L19.438 3.5h-2.69a.75.75 0 0 1-.742-.648l-.007-.102a.75.75 0 0 1 .648-.743L16.75 2zM3.75 17a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5a.75.75 0 0 1 .75-.75zm5.75-3.25a.75.75 0 0 0-1.5 0v7.5a.75.75 0 0 0 1.5 0v-7.5zM13.75 15a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0v-5.5a.75.75 0 0 1 .75-.75zm5.75-4.25a.75.75 0 0 0-1.5 0v10.5a.75.75 0 0 0 1.5 0v-10.5z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_CONVERSATION_REPORTS = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.587-1.112l-3.826 1.067a1.25 1.25 0 0 1-1.54-1.54l1.068-3.823A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2Zm0 1.5A8.5 8.5 0 0 0 3.5 12c0 1.47.373 2.883 1.073 4.137l.15.27-1.112 3.984 3.987-1.112.27.15A8.5 8.5 0 1 0 12 3.5ZM8.75 13h4.498a.75.75 0 0 1 .102 1.493l-.102.007H8.75a.75.75 0 0 1-.102-1.493L8.75 13h4.498H8.75Zm0-3.5h6.505a.75.75 0 0 1 .101 1.493l-.101.007H8.75a.75.75 0 0 1-.102-1.493L8.75 9.5h6.505H8.75Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_AGENT_REPORTS = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M4 13.999L13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14zM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78zM4 15.499l-.1.01a.51.51 0 0 0-.254.136a.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242c.826.445 2.003.714 3.266.753l.317.005l.317-.005c1.263-.039 2.439-.308 3.266-.753c.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001zM8.5 3a4.5 4.5 0 1 1 0 9a4.5 4.5 0 0 1 0-9zm9 2a3.5 3.5 0 1 1 0 7a3.5 3.5 0 0 1 0-7zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3s3-1.346 3-3s-1.346-3-3-3zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2s2-.897 2-2s-.897-2-2-2z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_LABEL_REPORTS = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M19.75 2A2.25 2.25 0 0 1 22 4.25v5.462a3.25 3.25 0 0 1-.952 2.298l-8.5 8.503a3.255 3.255 0 0 1-4.597.001L3.489 16.06a3.25 3.25 0 0 1-.003-4.596l8.5-8.51A3.25 3.25 0 0 1 14.284 2h5.465zm0 1.5h-5.465c-.465 0-.91.185-1.239.513l-8.512 8.523a1.75 1.75 0 0 0 .015 2.462l4.461 4.454a1.755 1.755 0 0 0 2.477 0l8.5-8.503a1.75 1.75 0 0 0 .513-1.237V4.25a.75.75 0 0 0-.75-.75zM17 5.502a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_INBOX_REPORTS = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M6.25 3h11.5a3.25 3.25 0 0 1 3.245 3.066L21 6.25v11.5a3.25 3.25 0 0 1-3.066 3.245L17.75 21H6.25a3.25 3.25 0 0 1-3.245-3.066L3 17.75V6.25a3.25 3.25 0 0 1 3.066-3.245L6.25 3h11.5h-11.5zM4.5 14.5v3.25a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V14.5h-3.825a3.752 3.752 0 0 1-3.475 2.995l-.2.005a3.752 3.752 0 0 1-3.632-2.812l-.043-.188H4.5v3.25v-3.25zm13.25-10H6.25a1.75 1.75 0 0 0-1.744 1.606L4.5 6.25V13H9a.75.75 0 0 1 .743.648l.007.102a2.25 2.25 0 0 0 4.495.154l.005-.154a.75.75 0 0 1 .648-.743L15 13h4.5V6.25a1.75 1.75 0 0 0-1.607-1.744L17.75 4.5z" fill="currentColor"></path></g></svg>`;
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
ICON_REPORTS_OVERVIEW,
|
||||
ICON_TEAM_REPORTS,
|
||||
ICON_USER_PROFILE,
|
||||
ICON_CONVERSATION_REPORTS,
|
||||
} from './CommandBarIcons';
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
|
||||
|
@ -41,6 +42,14 @@ const GO_TO_COMMANDS = [
|
|||
path: accountId => `accounts/${accountId}/reports/overview`,
|
||||
role: ['administrator'],
|
||||
},
|
||||
{
|
||||
id: 'open_conversation_reports',
|
||||
section: 'COMMAND_BAR.SECTIONS.REPORTS',
|
||||
title: 'COMMAND_BAR.COMMANDS.GO_TO_CONVERSATION_REPORTS',
|
||||
icon: ICON_CONVERSATION_REPORTS,
|
||||
path: accountId => `accounts/${accountId}/reports/conversation`,
|
||||
role: ['administrator'],
|
||||
},
|
||||
{
|
||||
id: 'open_agent_reports',
|
||||
section: 'COMMAND_BAR.SECTIONS.REPORTS',
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
<template>
|
||||
<div class="column content-box">
|
||||
<div class="row">
|
||||
<div class="column small-12 medium-8 conversation-metric">
|
||||
<metric-card
|
||||
:header="this.$t('OVERVIEW_REPORTS.ACCOUNT_CONVERSATIONS.HEADER')"
|
||||
:is-loading="uiFlags.isFetchingAccountConversationMetric"
|
||||
:loading-message="
|
||||
$t('OVERVIEW_REPORTS.ACCOUNT_CONVERSATIONS.LOADING_MESSAGE')
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(metric, name, index) in conversationMetrics"
|
||||
:key="index"
|
||||
class="metric-content column"
|
||||
>
|
||||
<h3 class="heading">
|
||||
{{ name }}
|
||||
</h3>
|
||||
<p class="metric">{{ metric }}</p>
|
||||
</div>
|
||||
</metric-card>
|
||||
</div>
|
||||
<div class="column small-12 medium-4">
|
||||
<metric-card :header="this.$t('OVERVIEW_REPORTS.AGENT_STATUS.HEADER')">
|
||||
<div
|
||||
v-for="(metric, name, index) in agentStatusMetrics"
|
||||
:key="index"
|
||||
class="metric-content column"
|
||||
>
|
||||
<h3 class="heading">
|
||||
{{ name }}
|
||||
</h3>
|
||||
<p class="metric">{{ metric }}</p>
|
||||
</div>
|
||||
</metric-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<metric-card
|
||||
:header="this.$t('OVERVIEW_REPORTS.AGENT_CONVERSATIONS.HEADER')"
|
||||
>
|
||||
<agent-table
|
||||
:total-agents="agentsCount"
|
||||
:agent-metrics="agentConversationMetric"
|
||||
:page-index="pageIndex"
|
||||
:is-loading="uiFlags.isFetchingAgentConversationMetric"
|
||||
@page-change="onPageNumberChange"
|
||||
/>
|
||||
</metric-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import AgentTable from './components/overview/AgentTable';
|
||||
import MetricCard from './components/overview/MetricCard';
|
||||
import { OVERVIEW_METRICS } from './constants';
|
||||
export default {
|
||||
name: 'LiveReports',
|
||||
components: {
|
||||
AgentTable,
|
||||
MetricCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageIndex: 1,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
agentStatus: 'agents/getAgentStatus',
|
||||
agentsCount: 'agents/getAgentsCount',
|
||||
accountConversationMetric: 'getAccountConversationMetric',
|
||||
agentConversationMetric: 'getAgentConversationMetric',
|
||||
uiFlags: 'getOverviewUIFlags',
|
||||
}),
|
||||
agentStatusMetrics() {
|
||||
let metric = {};
|
||||
Object.keys(this.agentStatus).forEach(key => {
|
||||
const metricName = this.$t(
|
||||
`OVERVIEW_REPORTS.AGENT_STATUS.${OVERVIEW_METRICS[key]}`
|
||||
);
|
||||
metric[metricName] = this.agentStatus[key];
|
||||
});
|
||||
return metric;
|
||||
},
|
||||
conversationMetrics() {
|
||||
let metric = {};
|
||||
Object.keys(this.accountConversationMetric).forEach(key => {
|
||||
const metricName = this.$t(
|
||||
`OVERVIEW_REPORTS.ACCOUNT_CONVERSATIONS.${OVERVIEW_METRICS[key]}`
|
||||
);
|
||||
metric[metricName] = this.accountConversationMetric[key];
|
||||
});
|
||||
return metric;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('agents/get');
|
||||
this.fetchAllData();
|
||||
|
||||
bus.$on('fetch_overview_reports', () => {
|
||||
this.fetchAllData();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
fetchAllData() {
|
||||
this.fetchAccountConversationMetric();
|
||||
this.fetchAgentConversationMetric();
|
||||
},
|
||||
fetchAccountConversationMetric() {
|
||||
this.$store.dispatch('fetchAccountConversationMetric', {
|
||||
type: 'account',
|
||||
});
|
||||
},
|
||||
fetchAgentConversationMetric() {
|
||||
this.$store.dispatch('fetchAgentConversationMetric', {
|
||||
type: 'agent',
|
||||
page: this.pageIndex,
|
||||
});
|
||||
},
|
||||
onPageNumberChange(pageIndex) {
|
||||
this.pageIndex = pageIndex;
|
||||
this.fetchAgentConversationMetric();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,199 @@
|
|||
<template>
|
||||
<div class="agent-table-container">
|
||||
<ve-table
|
||||
max-height="calc(100vh - 35rem)"
|
||||
:fixed-header="true"
|
||||
:columns="columns"
|
||||
:table-data="tableData"
|
||||
/>
|
||||
<div v-if="isLoading" class="agents-loader">
|
||||
<spinner />
|
||||
<span>{{
|
||||
$t('OVERVIEW_REPORTS.AGENT_CONVERSATIONS.LOADING_MESSAGE')
|
||||
}}</span>
|
||||
</div>
|
||||
<empty-state
|
||||
v-else-if="!isLoading && !agentMetrics.length"
|
||||
:title="$t('OVERVIEW_REPORTS.AGENT_CONVERSATIONS.NO_AGENTS')"
|
||||
/>
|
||||
<div v-if="agentMetrics.length > 0" class="table-pagination">
|
||||
<ve-pagination
|
||||
:total="totalAgents"
|
||||
:page-index="pageIndex"
|
||||
:page-size="10"
|
||||
:page-size-option="[10]"
|
||||
@on-page-number-change="onPageNumberChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { VeTable, VePagination } from 'vue-easytable';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
|
||||
export default {
|
||||
name: 'AgentTable',
|
||||
components: {
|
||||
EmptyState,
|
||||
Spinner,
|
||||
VeTable,
|
||||
VePagination,
|
||||
},
|
||||
props: {
|
||||
totalAgents: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
agentMetrics: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
pageIndex: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
tableData() {
|
||||
return this.agentMetrics.map(agent => {
|
||||
return {
|
||||
agent: agent.name,
|
||||
email: agent.email,
|
||||
thumbnail: agent.thumbnail,
|
||||
total: agent.metric.total || 0,
|
||||
unattended: agent.metric.unattended || 0,
|
||||
status: agent.availability,
|
||||
};
|
||||
});
|
||||
},
|
||||
columns() {
|
||||
return [
|
||||
{
|
||||
field: 'agent',
|
||||
key: 'agent',
|
||||
title: this.$t(
|
||||
'OVERVIEW_REPORTS.AGENT_CONVERSATIONS.TABLE_HEADER.AGENT'
|
||||
),
|
||||
fixed: 'left',
|
||||
align: 'left',
|
||||
width: 25,
|
||||
renderBodyCell: ({ row }) => (
|
||||
<div class="row-user-block">
|
||||
<Thumbnail
|
||||
src={row.thumbnail}
|
||||
size="32px"
|
||||
username={row.agent}
|
||||
status={row.status}
|
||||
/>
|
||||
<div class="user-block">
|
||||
<h6 class="title text-truncate">{row.agent}</h6>
|
||||
<span class="sub-title">{row.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'total',
|
||||
key: 'total',
|
||||
title: this.$t(
|
||||
'OVERVIEW_REPORTS.AGENT_CONVERSATIONS.TABLE_HEADER.TOTAL'
|
||||
),
|
||||
align: 'left',
|
||||
width: 10,
|
||||
},
|
||||
{
|
||||
field: 'unattended',
|
||||
key: 'unattended',
|
||||
title: this.$t(
|
||||
'OVERVIEW_REPORTS.AGENT_CONVERSATIONS.TABLE_HEADER.UNATTENDED'
|
||||
),
|
||||
align: 'left',
|
||||
width: 10,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
onPageNumberChange(pageIndex) {
|
||||
this.$emit('page-change', pageIndex);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.agent-table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.ve-table {
|
||||
&::v-deep {
|
||||
th.ve-table-header-th {
|
||||
font-size: var(--font-size-mini) !important;
|
||||
padding: var(--space-small) var(--space-two) !important;
|
||||
}
|
||||
|
||||
td.ve-table-body-td {
|
||||
padding: var(--space-one) var(--space-two) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::v-deep .ve-pagination {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::v-deep .ve-pagination-select {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.row-user-block {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
text-align: left;
|
||||
|
||||
.user-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
.title {
|
||||
font-size: var(--font-size-small);
|
||||
margin: var(--zero);
|
||||
line-height: 1;
|
||||
}
|
||||
.sub-title {
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
}
|
||||
|
||||
.user-thumbnail-box {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
}
|
||||
|
||||
.table-pagination {
|
||||
margin-top: var(--space-normal);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.agents-loader {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: var(--font-size-default);
|
||||
justify-content: center;
|
||||
padding: var(--space-large);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,98 @@
|
|||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>{{ header }}</h5>
|
||||
<span class="live">
|
||||
<span class="ellipse" /><span>{{ $t('OVERVIEW_REPORTS.LIVE') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="!isLoading" class="card-body row">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div v-else-if="isLoading" class="conversation-metric-loader">
|
||||
<spinner />
|
||||
<span>{{ loadingMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
|
||||
export default {
|
||||
name: 'MetricCard',
|
||||
components: {
|
||||
Spinner,
|
||||
},
|
||||
props: {
|
||||
header: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loadingMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
margin: var(--space-small) !important;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-medium);
|
||||
|
||||
h5 {
|
||||
margin-bottom: var(--zero);
|
||||
}
|
||||
|
||||
.live {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-right: var(--space-small);
|
||||
padding-left: var(--space-small);
|
||||
margin: var(--space-smaller);
|
||||
background: rgba(37, 211, 102, 0.1);
|
||||
color: var(--g-400);
|
||||
font-size: var(--font-size-mini);
|
||||
|
||||
.ellipse {
|
||||
background-color: var(--g-400);
|
||||
height: var(--space-smaller);
|
||||
width: var(--space-smaller);
|
||||
border-radius: var(--border-radius-rounded);
|
||||
margin-right: var(--space-smaller);
|
||||
}
|
||||
}
|
||||
}
|
||||
.card-body {
|
||||
.metric-content {
|
||||
padding-bottom: var(--space-small);
|
||||
.heading {
|
||||
font-size: var(--font-size-default);
|
||||
}
|
||||
.metric {
|
||||
color: var(--w-800);
|
||||
font-size: var(--font-size-bigger);
|
||||
margin-bottom: var(--zero);
|
||||
margin-top: var(--space-smaller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-metric-loader {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: var(--font-size-default);
|
||||
justify-content: center;
|
||||
padding: var(--space-large);
|
||||
}
|
||||
</style>
|
|
@ -121,3 +121,12 @@ export const METRIC_CHART = {
|
|||
},
|
||||
resolutions_count: DEFAULT_CHART,
|
||||
};
|
||||
|
||||
export const OVERVIEW_METRICS = {
|
||||
total: 'TOTAL',
|
||||
unattended: 'UNATTENDED',
|
||||
unassigned: 'UNASSIGNED',
|
||||
online: 'ONLINE',
|
||||
busy: 'BUSY',
|
||||
offline: 'OFFLINE',
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import LabelReports from './LabelReports';
|
|||
import InboxReports from './InboxReports';
|
||||
import TeamReports from './TeamReports';
|
||||
import CsatResponses from './CsatResponses';
|
||||
import LiveReports from './LiveReports';
|
||||
import SettingsContent from '../Wrapper';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
|
@ -13,7 +14,7 @@ export default {
|
|||
path: frontendURL('accounts/:accountId/reports'),
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'REPORT.HEADER',
|
||||
headerTitle: 'OVERVIEW_REPORTS.HEADER',
|
||||
icon: 'arrow-trending-lines',
|
||||
keepAlive: false,
|
||||
},
|
||||
|
@ -24,7 +25,24 @@ export default {
|
|||
},
|
||||
{
|
||||
path: 'overview',
|
||||
name: 'settings_account_reports',
|
||||
name: 'account_overview_reports',
|
||||
roles: ['administrator'],
|
||||
component: LiveReports,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/reports'),
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'REPORT.HEADER',
|
||||
icon: 'chat',
|
||||
keepAlive: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'conversation',
|
||||
name: 'conversation_reports',
|
||||
roles: ['administrator'],
|
||||
component: Index,
|
||||
},
|
||||
|
|
|
@ -22,6 +22,22 @@ export const getters = {
|
|||
getUIFlags($state) {
|
||||
return $state.uiFlags;
|
||||
},
|
||||
getAgentStatus($state) {
|
||||
let status = {
|
||||
online: $state.records.filter(
|
||||
agent => agent.availability_status === 'online'
|
||||
).length,
|
||||
busy: $state.records.filter(agent => agent.availability_status === 'busy')
|
||||
.length,
|
||||
offline: $state.records.filter(
|
||||
agent => agent.availability_status === 'offline'
|
||||
).length,
|
||||
};
|
||||
return status;
|
||||
},
|
||||
getAgentsCount($state) {
|
||||
return $state.records.length;
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
|
@ -58,9 +74,10 @@ export const actions = {
|
|||
}
|
||||
},
|
||||
|
||||
updatePresence: async ({ commit }, data) => {
|
||||
updatePresence: async ({ commit, dispatch }, data) => {
|
||||
commit(types.default.SET_AGENT_UPDATING_STATUS, true);
|
||||
commit(types.default.UPDATE_AGENTS_PRESENCE, data);
|
||||
dispatch('updateReportAgentStatus', data, { root: true });
|
||||
commit(types.default.SET_AGENT_UPDATING_STATUS, false);
|
||||
},
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
/* eslint no-shadow: 0 */
|
||||
import * as types from '../mutation-types';
|
||||
import Report from '../../api/reports';
|
||||
import Vue from 'vue';
|
||||
|
||||
import { downloadCsvFile } from '../../helper/downloadCsvFile';
|
||||
|
||||
|
@ -22,6 +23,14 @@ const state = {
|
|||
resolutions_count: 0,
|
||||
previous: {},
|
||||
},
|
||||
overview: {
|
||||
uiFlags: {
|
||||
isFetchingAccountConversationMetric: false,
|
||||
isFetchingAgentConversationMetric: false,
|
||||
},
|
||||
accountConversationMetric: {},
|
||||
agentConversationMetric: [],
|
||||
},
|
||||
};
|
||||
|
||||
const getters = {
|
||||
|
@ -31,6 +40,15 @@ const getters = {
|
|||
getAccountSummary(_state) {
|
||||
return _state.accountSummary;
|
||||
},
|
||||
getAccountConversationMetric(_state) {
|
||||
return _state.overview.accountConversationMetric;
|
||||
},
|
||||
getAgentConversationMetric(_state) {
|
||||
return _state.overview.agentConversationMetric;
|
||||
},
|
||||
getOverviewUIFlags($state) {
|
||||
return $state.overview.uiFlags;
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
|
@ -70,6 +88,37 @@ export const actions = {
|
|||
commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, false);
|
||||
});
|
||||
},
|
||||
fetchAccountConversationMetric({ commit }, reportObj) {
|
||||
commit(types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING, true);
|
||||
Report.getConversationMetric(reportObj.type)
|
||||
.then(accountConversationMetric => {
|
||||
commit(
|
||||
types.default.SET_ACCOUNT_CONVERSATION_METRIC,
|
||||
accountConversationMetric.data
|
||||
);
|
||||
commit(types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING, false);
|
||||
})
|
||||
.catch(() => {
|
||||
commit(types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING, false);
|
||||
});
|
||||
},
|
||||
fetchAgentConversationMetric({ commit }, reportObj) {
|
||||
commit(types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING, true);
|
||||
Report.getConversationMetric(reportObj.type, reportObj.page)
|
||||
.then(agentConversationMetric => {
|
||||
commit(
|
||||
types.default.SET_AGENT_CONVERSATION_METRIC,
|
||||
agentConversationMetric.data
|
||||
);
|
||||
commit(types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING, false);
|
||||
})
|
||||
.catch(() => {
|
||||
commit(types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING, false);
|
||||
});
|
||||
},
|
||||
updateReportAgentStatus({ commit }, data) {
|
||||
commit(types.default.UPDATE_REPORT_AGENTS_STATUS, data);
|
||||
},
|
||||
downloadAgentReports(_, reportObj) {
|
||||
return Report.getAgentReports(reportObj.from, reportObj.to)
|
||||
.then(response => {
|
||||
|
@ -118,6 +167,28 @@ const mutations = {
|
|||
[types.default.SET_ACCOUNT_SUMMARY](_state, summaryData) {
|
||||
_state.accountSummary = summaryData;
|
||||
},
|
||||
[types.default.SET_ACCOUNT_CONVERSATION_METRIC](_state, metricData) {
|
||||
_state.overview.accountConversationMetric = metricData;
|
||||
},
|
||||
[types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING](_state, flag) {
|
||||
_state.overview.uiFlags.isFetchingAccountConversationMetric = flag;
|
||||
},
|
||||
[types.default.SET_AGENT_CONVERSATION_METRIC](_state, metricData) {
|
||||
_state.overview.agentConversationMetric = metricData;
|
||||
},
|
||||
[types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING](_state, flag) {
|
||||
_state.overview.uiFlags.isFetchingAgentConversationMetric = flag;
|
||||
},
|
||||
[types.default.UPDATE_REPORT_AGENTS_STATUS](_state, data) {
|
||||
_state.overview.agentConversationMetric.forEach((element, index) => {
|
||||
const availabilityStatus = data[element.id];
|
||||
Vue.set(
|
||||
_state.overview.agentConversationMetric[index],
|
||||
'availability',
|
||||
availabilityStatus || 'offline'
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -4,6 +4,7 @@ import * as types from '../../../mutation-types';
|
|||
import agentList from './fixtures';
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
global.axios = axios;
|
||||
jest.mock('axios');
|
||||
|
||||
|
@ -97,7 +98,7 @@ describe('#actions', () => {
|
|||
describe('#updatePresence', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const data = { users: { 1: 'online' }, contacts: { 2: 'online' } };
|
||||
actions.updatePresence({ commit }, data);
|
||||
actions.updatePresence({ commit, dispatch }, data);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_AGENT_UPDATING_STATUS, true],
|
||||
[types.default.UPDATE_AGENTS_PRESENCE, data],
|
||||
|
|
|
@ -77,4 +77,71 @@ describe('#getters', () => {
|
|||
isDeleting: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('getAgentStatus', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Agent 1',
|
||||
email: 'agent1@chatwoot.com',
|
||||
confirmed: true,
|
||||
availability_status: 'online',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Agent 2',
|
||||
email: 'agent2@chatwoot.com',
|
||||
confirmed: false,
|
||||
availability_status: 'offline',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(getters.getAgentStatus(state)).toEqual({
|
||||
online: 1,
|
||||
busy: 0,
|
||||
offline: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('getAgentStatus', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Agent 1',
|
||||
email: 'agent1@chatwoot.com',
|
||||
confirmed: true,
|
||||
availability_status: 'online',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Agent 2',
|
||||
email: 'agent2@chatwoot.com',
|
||||
confirmed: false,
|
||||
availability_status: 'offline',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(getters.getAgentStatus(state)).toEqual({
|
||||
online: 1,
|
||||
busy: 0,
|
||||
offline: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('getAgentStatus', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Agent 1',
|
||||
email: 'agent1@chatwoot.com',
|
||||
confirmed: true,
|
||||
availability_status: 'online',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(getters.getAgentsCount(state)).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -92,6 +92,7 @@ describe('#mutations', () => {
|
|||
id: 2,
|
||||
name: 'Agent1',
|
||||
email: 'agent1@chatwoot.com',
|
||||
availability_status: 'offline',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -144,6 +144,13 @@ export default {
|
|||
SET_ACCOUNT_REPORTS: 'SET_ACCOUNT_REPORTS',
|
||||
SET_ACCOUNT_SUMMARY: 'SET_ACCOUNT_SUMMARY',
|
||||
TOGGLE_ACCOUNT_REPORT_LOADING: 'TOGGLE_ACCOUNT_REPORT_LOADING',
|
||||
SET_ACCOUNT_CONVERSATION_METRIC: 'SET_ACCOUNT_CONVERSATION_METRIC',
|
||||
TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING:
|
||||
'TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING',
|
||||
SET_AGENT_CONVERSATION_METRIC: 'SET_AGENT_CONVERSATION_METRIC',
|
||||
TOGGLE_AGENT_CONVERSATION_METRIC_LOADING:
|
||||
'TOGGLE_AGENT_CONVERSATION_METRIC_LOADING',
|
||||
UPDATE_REPORT_AGENTS_STATUS: 'UPDATE_AGENTS_STATUS',
|
||||
|
||||
// Conversation Metadata
|
||||
SET_CONVERSATION_METADATA: 'SET_CONVERSATION_METADATA',
|
||||
|
|
|
@ -37,11 +37,11 @@ export const updateAttributes = (state, data) => {
|
|||
export const updatePresence = (state, data) => {
|
||||
state.records.forEach((element, index) => {
|
||||
const availabilityStatus = data[element.id];
|
||||
if (availabilityStatus) {
|
||||
Vue.set(state.records[index], 'availability_status', availabilityStatus);
|
||||
} else {
|
||||
Vue.delete(state.records[index], 'availability_status');
|
||||
}
|
||||
Vue.set(
|
||||
state.records[index],
|
||||
'availability_status',
|
||||
availabilityStatus || 'offline'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -47,11 +47,13 @@ describe('#mutations', () => {
|
|||
id: 3,
|
||||
name: 'Pranav',
|
||||
avatar_url: '',
|
||||
availability_status: 'offline',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Nithin',
|
||||
avatar_url: '',
|
||||
availability_status: 'offline',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -23,6 +23,14 @@ class ActionCableListener < BaseListener
|
|||
broadcast(account, tokens, MESSAGE_UPDATED, message.push_event_data)
|
||||
end
|
||||
|
||||
def first_reply_created(event)
|
||||
message, account = extract_message_and_account(event)
|
||||
conversation = message.conversation
|
||||
tokens = user_tokens(account, conversation.inbox.members)
|
||||
|
||||
broadcast(account, tokens, FIRST_REPLY_CREATED, message.push_event_data)
|
||||
end
|
||||
|
||||
def conversation_created(event)
|
||||
conversation, account = extract_conversation_and_account(event)
|
||||
tokens = user_tokens(account, conversation.inbox.members) + contact_inbox_tokens(conversation.contact_inbox)
|
||||
|
|
|
@ -73,7 +73,7 @@ RSpec.describe 'Reports API', type: :request do
|
|||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
|
||||
expect(json_response['open']).to eq(11)
|
||||
expect(json_response['total']).to eq(11)
|
||||
expect(json_response['unattended']).to eq(11)
|
||||
expect(json_response['unassigned']).to eq(1)
|
||||
end
|
||||
|
@ -93,10 +93,10 @@ RSpec.describe 'Reports API', type: :request do
|
|||
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response.blank?).to be false
|
||||
user_metrics = json_response.find { |item| item['user']['name'] == admin[:name] }
|
||||
user_metrics = json_response.find { |item| item['name'] == admin[:name] }
|
||||
expect(user_metrics.present?).to be true
|
||||
|
||||
expect(user_metrics['metric']['open']).to eq(2)
|
||||
expect(user_metrics['metric']['total']).to eq(2)
|
||||
expect(user_metrics['metric']['unattended']).to eq(2)
|
||||
end
|
||||
|
||||
|
@ -116,7 +116,7 @@ RSpec.describe 'Reports API', type: :request do
|
|||
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response.blank?).to be false
|
||||
expect(json_response[0]['metric']['open']).to eq(10)
|
||||
expect(json_response[0]['metric']['total']).to eq(10)
|
||||
expect(json_response[0]['metric']['unattended']).to eq(10)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
type: object
|
||||
properties:
|
||||
user:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
name:
|
||||
type: string
|
||||
thumbnail:
|
||||
type: string
|
||||
id:
|
||||
type: number
|
||||
name:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
thumbnail:
|
||||
type: string
|
||||
availability:
|
||||
type: string
|
||||
metric:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
@ -5629,19 +5629,20 @@
|
|||
"agent_conversation_metrics": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"thumbnail": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"thumbnail": {
|
||||
"type": "string"
|
||||
},
|
||||
"availability": {
|
||||
"type": "string"
|
||||
},
|
||||
"metric": {
|
||||
"type": "object",
|
||||
|
|
Loading…
Reference in a new issue