From 676796ddc75a4a53282d544270890f2aa0b77264 Mon Sep 17 00:00:00 2001 From: "Aswin Dev P.S" Date: Mon, 25 Apr 2022 20:04:41 +0530 Subject: [PATCH] feat: Add reports about live agent load (#4537) * feat: Add reports about live agent load --- app/builders/v2/report_builder.rb | 16 +- .../api/v2/accounts/reports_controller.rb | 3 +- app/javascript/dashboard/api/reports.js | 9 + .../dashboard/api/specs/reports.spec.js | 13 ++ .../layout/config/sidebarItems/reports.js | 12 +- .../dashboard/helper/actionCable.js | 6 + .../i18n/locale/en/generalSettings.json | 1 + .../dashboard/i18n/locale/en/report.json | 30 ++- .../dashboard/i18n/locale/en/settings.json | 7 +- .../dashboard/commands/CommandBarIcons.js | 1 + .../dashboard/commands/goToCommandHotKeys.js | 9 + .../settings/reports/LiveReports.vue | 129 ++++++++++++ .../components/overview/AgentTable.vue | 199 ++++++++++++++++++ .../components/overview/MetricCard.vue | 98 +++++++++ .../dashboard/settings/reports/constants.js | 9 + .../settings/reports/reports.routes.js | 22 +- .../dashboard/store/modules/agents.js | 19 +- .../dashboard/store/modules/reports.js | 71 +++++++ .../modules/specs/agents/actions.spec.js | 3 +- .../modules/specs/agents/getters.spec.js | 67 ++++++ .../modules/specs/agents/mutations.spec.js | 1 + .../dashboard/store/mutation-types.js | 7 + .../shared/helpers/vuex/mutationHelpers.js | 10 +- .../modules/specs/agent/mutations.spec.js | 2 + app/listeners/action_cable_listener.rb | 8 + .../api/v2/accounts/report_controller_spec.rb | 8 +- .../resource/reports/conversation/agent.yml | 19 +- swagger/swagger.json | 27 +-- 28 files changed, 758 insertions(+), 48 deletions(-) create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/LiveReports.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/AgentTable.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/MetricCard.vue diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb index 67b80b7a5..459c0759a 100644 --- a/app/builders/v2/report_builder.rb +++ b/app/builders/v2/report_builder.rb @@ -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) diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index 227ba2500..c2117583a 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -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 diff --git a/app/javascript/dashboard/api/reports.js b/app/javascript/dashboard/api/reports.js index 5c39ddbe6..ca50c062f 100644 --- a/app/javascript/dashboard/api/reports.js +++ b/app/javascript/dashboard/api/reports.js @@ -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 }, diff --git a/app/javascript/dashboard/api/specs/reports.spec.js b/app/javascript/dashboard/api/specs/reports.spec.js index efde84fe4..b51c87db5 100644 --- a/app/javascript/dashboard/api/specs/reports.spec.js +++ b/app/javascript/dashboard/api/specs/reports.spec.js @@ -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, + }, + } + ); + }); }); }); diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js b/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js index a9c703c44..967ee44ed 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js @@ -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', diff --git a/app/javascript/dashboard/helper/actionCable.js b/app/javascript/dashboard/helper/actionCable.js index 77c8a7aff..e00cf283f 100644 --- a/app/javascript/dashboard/helper/actionCable.js +++ b/app/javascript/dashboard/helper/actionCable.js @@ -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 { diff --git a/app/javascript/dashboard/i18n/locale/en/generalSettings.json b/app/javascript/dashboard/i18n/locale/en/generalSettings.json index b1e1c3eca..2a726b26f 100644 --- a/app/javascript/dashboard/i18n/locale/en/generalSettings.json +++ b/app/javascript/dashboard/i18n/locale/en/generalSettings.json @@ -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", diff --git a/app/javascript/dashboard/i18n/locale/en/report.json b/app/javascript/dashboard/i18n/locale/en/report.json index 1574b1cca..a774dff33 100644 --- a/app/javascript/dashboard/i18n/locale/en/report.json +++ b/app/javascript/dashboard/i18n/locale/en/report.json @@ -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" + } } } diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 63b070b0e..97539d49c 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -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.", diff --git a/app/javascript/dashboard/routes/dashboard/commands/CommandBarIcons.js b/app/javascript/dashboard/routes/dashboard/commands/CommandBarIcons.js index 561e8cd0d..d0d0bb633 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/CommandBarIcons.js +++ b/app/javascript/dashboard/routes/dashboard/commands/CommandBarIcons.js @@ -13,6 +13,7 @@ export const ICON_SNOOZE_UNTIL_TOMORRROW = ``; export const ICON_CONTACT_DASHBOARD = ``; export const ICON_REPORTS_OVERVIEW = ``; +export const ICON_CONVERSATION_REPORTS = ``; export const ICON_AGENT_REPORTS = ``; export const ICON_LABEL_REPORTS = ``; export const ICON_INBOX_REPORTS = ``; diff --git a/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js b/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js index 5fee0f6f2..9a5b09f2e 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js +++ b/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js @@ -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', diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/LiveReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/LiveReports.vue new file mode 100644 index 000000000..88aa5c558 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/LiveReports.vue @@ -0,0 +1,129 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/AgentTable.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/AgentTable.vue new file mode 100644 index 000000000..55a93d2fe --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/AgentTable.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/MetricCard.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/MetricCard.vue new file mode 100644 index 000000000..5dd81f5ac --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/MetricCard.vue @@ -0,0 +1,98 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js b/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js index c44f2bdef..88f04b0f1 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js @@ -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', +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/reports.routes.js b/app/javascript/dashboard/routes/dashboard/settings/reports/reports.routes.js index 27f9a67ad..4d4814845 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/reports.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/reports.routes.js @@ -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, }, diff --git a/app/javascript/dashboard/store/modules/agents.js b/app/javascript/dashboard/store/modules/agents.js index 61be40559..b8dbc7fc4 100644 --- a/app/javascript/dashboard/store/modules/agents.js +++ b/app/javascript/dashboard/store/modules/agents.js @@ -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); }, diff --git a/app/javascript/dashboard/store/modules/reports.js b/app/javascript/dashboard/store/modules/reports.js index 0b31883bf..dc9a6c33b 100644 --- a/app/javascript/dashboard/store/modules/reports.js +++ b/app/javascript/dashboard/store/modules/reports.js @@ -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 { diff --git a/app/javascript/dashboard/store/modules/specs/agents/actions.spec.js b/app/javascript/dashboard/store/modules/specs/agents/actions.spec.js index 206ae9dfe..5c6310f48 100644 --- a/app/javascript/dashboard/store/modules/specs/agents/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/agents/actions.spec.js @@ -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], diff --git a/app/javascript/dashboard/store/modules/specs/agents/getters.spec.js b/app/javascript/dashboard/store/modules/specs/agents/getters.spec.js index 33a1ba5cf..b40bff351 100644 --- a/app/javascript/dashboard/store/modules/specs/agents/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/agents/getters.spec.js @@ -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); + }); }); diff --git a/app/javascript/dashboard/store/modules/specs/agents/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/agents/mutations.spec.js index 00b569aaf..5c918582d 100644 --- a/app/javascript/dashboard/store/modules/specs/agents/mutations.spec.js +++ b/app/javascript/dashboard/store/modules/specs/agents/mutations.spec.js @@ -92,6 +92,7 @@ describe('#mutations', () => { id: 2, name: 'Agent1', email: 'agent1@chatwoot.com', + availability_status: 'offline', }, ]); }); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 4ae2d0e10..f89a5205d 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -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', diff --git a/app/javascript/shared/helpers/vuex/mutationHelpers.js b/app/javascript/shared/helpers/vuex/mutationHelpers.js index 9822c0224..f54a1d21e 100644 --- a/app/javascript/shared/helpers/vuex/mutationHelpers.js +++ b/app/javascript/shared/helpers/vuex/mutationHelpers.js @@ -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' + ); }); }; diff --git a/app/javascript/widget/store/modules/specs/agent/mutations.spec.js b/app/javascript/widget/store/modules/specs/agent/mutations.spec.js index f29bb97d9..63bf3bcb3 100644 --- a/app/javascript/widget/store/modules/specs/agent/mutations.spec.js +++ b/app/javascript/widget/store/modules/specs/agent/mutations.spec.js @@ -47,11 +47,13 @@ describe('#mutations', () => { id: 3, name: 'Pranav', avatar_url: '', + availability_status: 'offline', }, { id: 4, name: 'Nithin', avatar_url: '', + availability_status: 'offline', }, ]); }); diff --git a/app/listeners/action_cable_listener.rb b/app/listeners/action_cable_listener.rb index d9099eda8..3d3bbdf83 100644 --- a/app/listeners/action_cable_listener.rb +++ b/app/listeners/action_cable_listener.rb @@ -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) diff --git a/spec/controllers/api/v2/accounts/report_controller_spec.rb b/spec/controllers/api/v2/accounts/report_controller_spec.rb index cb2b9b72d..8c6f989c8 100644 --- a/spec/controllers/api/v2/accounts/report_controller_spec.rb +++ b/spec/controllers/api/v2/accounts/report_controller_spec.rb @@ -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 diff --git a/swagger/definitions/resource/reports/conversation/agent.yml b/swagger/definitions/resource/reports/conversation/agent.yml index 7077b767b..251defb24 100644 --- a/swagger/definitions/resource/reports/conversation/agent.yml +++ b/swagger/definitions/resource/reports/conversation/agent.yml @@ -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: diff --git a/swagger/swagger.json b/swagger/swagger.json index 7ec216f9a..81f19149c 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -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",