From 6bd0e074dc2a1ec91f7b8396da8cecc1eb9a2180 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 25 May 2023 14:49:56 +0530 Subject: [PATCH] feat: Sort agents on availability status (#7174) --- .../conversation/contextMenu/Index.vue | 18 +- .../conversation/contextMenu/menuItem.vue | 1 + app/javascript/dashboard/mixins/agentMixin.js | 63 ++++-- .../dashboard/mixins/specs/agentFixtures.js | 194 +++++++++++++++++- .../dashboard/mixins/specs/agentMixin.spec.js | 101 ++++++++- 5 files changed, 354 insertions(+), 23 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue index ca7117cfc..643b2be03 100644 --- a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue +++ b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue @@ -79,6 +79,7 @@ import MenuItem from './menuItem.vue'; import MenuItemWithSubmenu from './menuItemWithSubmenu.vue'; import wootConstants from 'dashboard/constants/globals'; import snoozeTimesMixin from 'dashboard/mixins/conversation/snoozeTimesMixin'; +import agentMixin from 'dashboard/mixins/agentMixin'; import { mapGetters } from 'vuex'; import AgentLoadingPlaceholder from './agentLoadingPlaceholder.vue'; export default { @@ -87,7 +88,7 @@ export default { MenuItemWithSubmenu, AgentLoadingPlaceholder, }, - mixins: [snoozeTimesMixin], + mixins: [snoozeTimesMixin, agentMixin], props: { status: { type: String, @@ -202,6 +203,16 @@ export default { teams: 'teams/getTeams', assignableAgentsUiFlags: 'inboxAssignableAgents/getUIFlags', }), + filteredAgentOnAvailability() { + const agents = this.$store.getters[ + 'inboxAssignableAgents/getAssignableAgents' + ](this.inboxId); + const agentsByUpdatedPresence = this.getAgentsByUpdatedPresence(agents); + const filteredAgents = this.sortedAgentsByAvailability( + agentsByUpdatedPresence + ); + return filteredAgents; + }, assignableAgents() { return [ { @@ -212,9 +223,7 @@ export default { account_id: 0, email: 'None', }, - ...this.$store.getters['inboxAssignableAgents/getAssignableAgents']( - this.inboxId - ), + ...this.filteredAgentOnAvailability, ]; }, }, @@ -246,6 +255,7 @@ export default { ...(type === 'icon' && { icon: option.icon }), ...(type === 'label' && { color: option.color }), ...(type === 'agent' && { thumbnail: option.thumbnail }), + ...(type === 'agent' && { status: option.availability_status }), ...(type === 'text' && { label: option.label }), ...(type === 'label' && { label: option.title }), ...(type === 'agent' && { label: option.name }), diff --git a/app/javascript/dashboard/components/widgets/conversation/contextMenu/menuItem.vue b/app/javascript/dashboard/components/widgets/conversation/contextMenu/menuItem.vue index a2fc5eb7c..670d5a61a 100644 --- a/app/javascript/dashboard/components/widgets/conversation/contextMenu/menuItem.vue +++ b/app/javascript/dashboard/components/widgets/conversation/contextMenu/menuItem.vue @@ -15,6 +15,7 @@ v-if="variant === 'agent'" :username="option.label" :src="option.thumbnail" + :status="option.status" size="20px" class="agent-thumbnail" /> diff --git a/app/javascript/dashboard/mixins/agentMixin.js b/app/javascript/dashboard/mixins/agentMixin.js index 14c5bb043..1b3545a62 100644 --- a/app/javascript/dashboard/mixins/agentMixin.js +++ b/app/javascript/dashboard/mixins/agentMixin.js @@ -2,39 +2,68 @@ import { mapGetters } from 'vuex'; export default { computed: { + ...mapGetters({ + currentUser: 'getCurrentUser', + currentAccountId: 'getCurrentAccountId', + }), assignableAgents() { return this.$store.getters['inboxAssignableAgents/getAssignableAgents']( this.inboxId ); }, - ...mapGetters({ currentUser: 'getCurrentUser' }), isAgentSelected() { return this.currentChat?.meta?.assignee; }, + createNoneAgent() { + return { + confirmed: true, + name: 'None', + id: 0, + role: 'agent', + account_id: 0, + email: 'None', + }; + }, agentsList() { const agents = this.assignableAgents || []; - return [ - ...(this.isAgentSelected - ? [ - { - confirmed: true, - name: 'None', - id: 0, - role: 'agent', - account_id: 0, - email: 'None', - }, - ] - : []), - ...agents, - ].map(item => + const agentsByUpdatedPresence = this.getAgentsByUpdatedPresence(agents); + const none = this.createNoneAgent; + const filteredAgentsByAvailability = this.sortedAgentsByAvailability( + agentsByUpdatedPresence + ); + const filteredAgents = [ + ...(this.isAgentSelected ? [none] : []), + ...filteredAgentsByAvailability, + ]; + return filteredAgents; + }, + }, + methods: { + getAgentsByAvailability(agents, availability) { + return agents + .filter(agent => agent.availability_status === availability) + .sort((a, b) => a.name.localeCompare(b.name)); + }, + sortedAgentsByAvailability(agents) { + const onlineAgents = this.getAgentsByAvailability(agents, 'online'); + const busyAgents = this.getAgentsByAvailability(agents, 'busy'); + const offlineAgents = this.getAgentsByAvailability(agents, 'offline'); + const filteredAgents = [...onlineAgents, ...busyAgents, ...offlineAgents]; + return filteredAgents; + }, + getAgentsByUpdatedPresence(agents) { + // Here we are updating the availability status of the current user dynamically (live) based on the current account availability status + const agentsWithDynamicPresenceUpdate = agents.map(item => item.id === this.currentUser.id ? { ...item, - availability_status: this.currentUser.availability_status, + availability_status: this.currentUser.accounts.find( + account => account.id === this.currentAccountId + ).availability_status, } : item ); + return agentsWithDynamicPresenceUpdate; }, }, }; diff --git a/app/javascript/dashboard/mixins/specs/agentFixtures.js b/app/javascript/dashboard/mixins/specs/agentFixtures.js index 1c45b6034..58c360ed1 100644 --- a/app/javascript/dashboard/mixins/specs/agentFixtures.js +++ b/app/javascript/dashboard/mixins/specs/agentFixtures.js @@ -20,6 +20,36 @@ export default { name: 'Samuel Keta', role: 'agent', }, + { + account_id: 1, + availability_status: 'offline', + available_name: 'James K', + confirmed: true, + email: 'james@chatwoot.com', + id: 3, + name: 'James Koti', + role: 'agent', + }, + { + account_id: 1, + availability_status: 'busy', + available_name: 'Honey', + confirmed: true, + email: 'bee@chatwoot.com', + id: 4, + name: 'Honey Bee', + role: 'agent', + }, + { + account_id: 1, + availability_status: 'online', + available_name: 'Abraham', + confirmed: true, + email: 'abraham@chatwoot.com', + id: 5, + name: 'Abraham Keta', + role: 'agent', + }, ], formattedAgents: [ { @@ -32,7 +62,17 @@ export default { }, { account_id: 1, - availability_status: 'busy', + availability_status: 'online', + available_name: 'Abraham', + confirmed: true, + email: 'abraham@chatwoot.com', + id: 5, + name: 'Abraham Keta', + role: 'agent', + }, + { + account_id: 1, + availability_status: 'online', available_name: 'John K', confirmed: true, email: 'john@chatwoot.com', @@ -40,6 +80,70 @@ export default { name: 'John Kennady', role: 'administrator', }, + { + account_id: 1, + availability_status: 'busy', + available_name: 'Honey', + confirmed: true, + email: 'bee@chatwoot.com', + id: 4, + name: 'Honey Bee', + role: 'agent', + }, + { + account_id: 1, + availability_status: 'busy', + available_name: 'Samuel K', + confirmed: true, + email: 'samuel@chatwoot.com', + id: 2, + name: 'Samuel Keta', + role: 'agent', + }, + { + account_id: 1, + availability_status: 'offline', + available_name: 'James K', + confirmed: true, + email: 'james@chatwoot.com', + id: 3, + name: 'James Koti', + role: 'agent', + }, + ], + onlineAgents: [ + { + account_id: 1, + availability_status: 'online', + available_name: 'Abraham', + confirmed: true, + email: 'abraham@chatwoot.com', + id: 5, + name: 'Abraham Keta', + role: 'agent', + }, + { + account_id: 1, + availability_status: 'online', + available_name: 'John K', + confirmed: true, + email: 'john@chatwoot.com', + id: 1, + name: 'John Kennady', + role: 'administrator', + }, + ], + busyAgents: [ + { + account_id: 1, + availability_status: 'busy', + available_name: 'Honey', + confirmed: true, + email: 'bee@chatwoot.com', + id: 4, + name: 'Honey Bee', + role: 'agent', + }, { account_id: 1, availability_status: 'busy', @@ -51,4 +155,92 @@ export default { role: 'agent', }, ], + offlineAgents: [ + { + account_id: 1, + availability_status: 'offline', + available_name: 'James K', + confirmed: true, + email: 'james@chatwoot.com', + id: 3, + name: 'James Koti', + role: 'agent', + }, + ], + sortedByAvailability: [ + { + account_id: 1, + availability_status: 'online', + available_name: 'Abraham', + confirmed: true, + email: 'abraham@chatwoot.com', + id: 5, + name: 'Abraham Keta', + role: 'agent', + }, + { + account_id: 1, + availability_status: 'online', + available_name: 'John K', + confirmed: true, + email: 'john@chatwoot.com', + id: 1, + name: 'John Kennady', + role: 'administrator', + }, + { + account_id: 1, + availability_status: 'busy', + available_name: 'Honey', + confirmed: true, + email: 'bee@chatwoot.com', + id: 4, + name: 'Honey Bee', + role: 'agent', + }, + { + account_id: 1, + availability_status: 'busy', + available_name: 'Samuel K', + confirmed: true, + email: 'samuel@chatwoot.com', + id: 2, + name: 'Samuel Keta', + role: 'agent', + }, + { + account_id: 1, + availability_status: 'offline', + available_name: 'James K', + confirmed: true, + email: 'james@chatwoot.com', + id: 3, + name: 'James Koti', + role: 'agent', + }, + ], + formattedAgentsByPresenceOnline: [ + { + account_id: 1, + availability_status: 'online', + available_name: 'Abraham', + confirmed: true, + email: 'abr@chatwoot.com', + id: 1, + name: 'Abraham Keta', + role: 'agent', + }, + ], + formattedAgentsByPresenceOffline: [ + { + account_id: 1, + availability_status: 'offline', + available_name: 'Abraham', + confirmed: true, + email: 'abr@chatwoot.com', + id: 1, + name: 'Abraham Keta', + role: 'agent', + }, + ], }; diff --git a/app/javascript/dashboard/mixins/specs/agentMixin.spec.js b/app/javascript/dashboard/mixins/specs/agentMixin.spec.js index 7cd2ad0db..f92f48555 100644 --- a/app/javascript/dashboard/mixins/specs/agentMixin.spec.js +++ b/app/javascript/dashboard/mixins/specs/agentMixin.spec.js @@ -12,12 +12,71 @@ describe('agentMixin', () => { getters = { getCurrentUser: () => ({ id: 1, - availability_status: 'busy', + accounts: [ + { + id: 1, + availability_status: 'online', + auto_offline: false, + }, + ], }), + getCurrentAccountId: () => 1, }; store = new Vuex.Store({ getters }); }); + it('return agents by availability', () => { + const Component = { + render() {}, + title: 'TestComponent', + mixins: [agentMixin], + data() { + return { + inboxId: 1, + currentChat: { meta: { assignee: { name: 'John' } } }, + }; + }, + computed: { + assignableAgents() { + return agentFixtures.allAgents; + }, + }, + }; + const wrapper = shallowMount(Component, { store, localVue }); + expect( + wrapper.vm.getAgentsByAvailability(agentFixtures.allAgents, 'online') + ).toEqual(agentFixtures.onlineAgents); + expect( + wrapper.vm.getAgentsByAvailability(agentFixtures.allAgents, 'busy') + ).toEqual(agentFixtures.busyAgents); + expect( + wrapper.vm.getAgentsByAvailability(agentFixtures.allAgents, 'offline') + ).toEqual(agentFixtures.offlineAgents); + }); + + it('return sorted agents by availability', () => { + const Component = { + render() {}, + title: 'TestComponent', + mixins: [agentMixin], + data() { + return { + inboxId: 1, + currentChat: { meta: { assignee: { name: 'John' } } }, + }; + }, + computed: { + assignableAgents() { + return agentFixtures.allAgents; + }, + }, + }; + const wrapper = shallowMount(Component, { store, localVue }); + expect( + wrapper.vm.sortedAgentsByAvailability(agentFixtures.allAgents) + ).toEqual(agentFixtures.sortedByAvailability); + }); + it('return formatted agents', () => { const Component = { render() {}, @@ -38,4 +97,44 @@ describe('agentMixin', () => { const wrapper = shallowMount(Component, { store, localVue }); expect(wrapper.vm.agentsList).toEqual(agentFixtures.formattedAgents); }); + + it('return formatted agents by presence', () => { + const Component = { + render() {}, + title: 'TestComponent', + mixins: [agentMixin], + data() { + return { + inboxId: 1, + currentChat: { meta: { assignee: { name: 'John' } } }, + }; + }, + computed: { + currentUser() { + return { + id: 1, + accounts: [ + { + id: 1, + availability_status: 'offline', + auto_offline: false, + }, + ], + }; + }, + currentAccountId() { + return 1; + }, + assignableAgents() { + return agentFixtures.allAgents; + }, + }, + }; + const wrapper = shallowMount(Component, { store, localVue }); + expect( + wrapper.vm.getAgentsByUpdatedPresence( + agentFixtures.formattedAgentsByPresenceOnline + ) + ).toEqual(agentFixtures.formattedAgentsByPresenceOffline); + }); });