diff --git a/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb b/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb index c449ecf96..4439fcdfa 100644 --- a/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb +++ b/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb @@ -4,16 +4,33 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base RESULTS_PER_PAGE = 25 before_action :check_authorization - before_action :csat_survey_responses, only: [:index] + before_action :set_csat_survey_responses, only: [:index, :metrics] + before_action :set_current_page, only: [:index] + before_action :set_current_page_surveys, only: [:index] + before_action :set_total_sent_messages_count, only: [:metrics] def index; end + def metrics + @total_count = @csat_survey_responses.count + @ratings_count = @csat_survey_responses.group(:rating).count + end + private - def csat_survey_responses - @csat_survey_responses = Current.account.csat_survey_responses + def set_total_sent_messages_count + @csat_messages = Current.account.messages.input_csat + @csat_messages = @csat_messages.where(created_at: range) if range.present? + @total_sent_messages_count = @csat_messages.count + end + + def set_csat_survey_responses + @csat_survey_responses = Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact]) @csat_survey_responses = @csat_survey_responses.where(created_at: range) if range.present? - @csat_survey_responses.page(@current_page).per(RESULTS_PER_PAGE) + end + + def set_current_page_surveys + @csat_survey_responses = @csat_survey_responses.page(@current_page).per(RESULTS_PER_PAGE) end def set_current_page diff --git a/app/javascript/dashboard/api/csatReports.js b/app/javascript/dashboard/api/csatReports.js new file mode 100644 index 000000000..9198dcc26 --- /dev/null +++ b/app/javascript/dashboard/api/csatReports.js @@ -0,0 +1,18 @@ +/* global axios */ +import ApiClient from './ApiClient'; + +class CSATReportsAPI extends ApiClient { + constructor() { + super('csat_survey_responses', { accountScoped: true }); + } + + get({ page } = {}) { + return axios.get(this.url, { params: { page } }); + } + + getMetrics() { + return axios.get(`${this.url}/metrics`); + } +} + +export default new CSATReportsAPI(); diff --git a/app/javascript/dashboard/api/specs/csatReports.spec.js b/app/javascript/dashboard/api/specs/csatReports.spec.js new file mode 100644 index 000000000..7b7c36c45 --- /dev/null +++ b/app/javascript/dashboard/api/specs/csatReports.spec.js @@ -0,0 +1,29 @@ +import csatReportsAPI from '../csatReports'; +import ApiClient from '../ApiClient'; +import describeWithAPIMock from './apiSpecHelper'; + +describe('#Reports API', () => { + it('creates correct instance', () => { + expect(csatReportsAPI).toBeInstanceOf(ApiClient); + expect(csatReportsAPI.apiVersion).toBe('/api/v1'); + expect(csatReportsAPI).toHaveProperty('get'); + expect(csatReportsAPI).toHaveProperty('getMetrics'); + }); + describeWithAPIMock('API calls', context => { + it('#get', () => { + csatReportsAPI.get({ page: 1 }); + expect(context.axiosMock.get).toHaveBeenCalledWith( + '/api/v1/csat_survey_responses', + { + params: { page: 1 }, + } + ); + }); + it('#getMetrics', () => { + csatReportsAPI.getMetrics(); + expect(context.axiosMock.get).toHaveBeenCalledWith( + '/api/v1/csat_survey_responses/metrics' + ); + }); + }); +}); diff --git a/app/javascript/dashboard/assets/scss/_foundation-custom.scss b/app/javascript/dashboard/assets/scss/_foundation-custom.scss index 8a82e89c0..0e2329b66 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-custom.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-custom.scss @@ -42,6 +42,10 @@ code { white-space: nowrap; } +.text-capitalize { + text-transform: capitalize; +} + .cursor-pointer { cursor: pointer; } diff --git a/app/javascript/dashboard/assets/scss/widgets/_report.scss b/app/javascript/dashboard/assets/scss/widgets/_report.scss index 3e3c30390..ca013a39d 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_report.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_report.scss @@ -1,7 +1,6 @@ .report-card { @include padding($space-normal $space-small $space-normal $space-two); @include margin($zero); - @include background-light; cursor: pointer; @include custom-border-top(3px, transparent); diff --git a/app/javascript/dashboard/components/index.js b/app/javascript/dashboard/components/index.js index e5ec89da0..f737bd313 100644 --- a/app/javascript/dashboard/components/index.js +++ b/app/javascript/dashboard/components/index.js @@ -1,14 +1,14 @@ /* eslint no-plusplus: 0 */ -/* eslint-env browser */ import AvatarUploader from './widgets/forms/AvatarUploader.vue'; import Bar from './widgets/chart/BarChart'; import Button from './ui/WootButton'; import Code from './Code'; import ColorPicker from './widgets/ColorPicker'; -import DeleteModal from './widgets/modal/DeleteModal.vue'; import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue'; +import DeleteModal from './widgets/modal/DeleteModal.vue'; import DropdownItem from 'shared/components/ui/dropdown/DropdownItem'; import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu'; +import HorizontalBar from './widgets/chart/HorizontalBarChart'; import Input from './widgets/forms/Input.vue'; import Label from './ui/Label'; import LoadingState from './widgets/LoadingState'; @@ -28,12 +28,14 @@ const WootUIKit = { Button, Code, ColorPicker, + ConfirmDeleteModal, DeleteModal, DropdownItem, DropdownMenu, + HorizontalBar, Input, - LoadingState, Label, + LoadingState, Modal, ModalHeader, ReportStatsCard, @@ -43,7 +45,6 @@ const WootUIKit = { Tabs, TabsItem, Thumbnail, - ConfirmDeleteModal, install(Vue) { const keys = Object.keys(this); keys.pop(); // remove 'install' from keys diff --git a/app/javascript/dashboard/components/widgets/ReportStatsCard.vue b/app/javascript/dashboard/components/widgets/ReportStatsCard.vue index b8a0f19c7..aa989914b 100644 --- a/app/javascript/dashboard/components/widgets/ReportStatsCard.vue +++ b/app/javascript/dashboard/components/widgets/ReportStatsCard.vue @@ -1,12 +1,21 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetrics.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetrics.vue new file mode 100644 index 000000000..227b619d2 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetrics.vue @@ -0,0 +1,108 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatTable.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatTable.vue new file mode 100644 index 000000000..8c3984cd0 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatTable.vue @@ -0,0 +1,191 @@ + + + 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 49eb5592f..737c91e7b 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/reports.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/reports.routes.js @@ -1,4 +1,5 @@ import Index from './Index'; +import CsatResponses from './CsatResponses'; import SettingsContent from '../Wrapper'; import { frontendURL } from '../../../../helper/URLHelper'; @@ -9,17 +10,36 @@ export default { component: SettingsContent, props: { headerTitle: 'REPORT.HEADER', - headerButtonText: 'REPORT.HEADER_BTN_TXT', icon: 'ion-arrow-graph-up-right', }, children: [ { path: '', + redirect: 'overview', + }, + { + path: 'overview', name: 'settings_account_reports', roles: ['administrator'], component: Index, }, ], }, + { + path: frontendURL('accounts/:accountId/reports'), + component: SettingsContent, + props: { + headerTitle: 'CSAT_REPORTS.HEADER', + icon: 'ion-happy-outline', + }, + children: [ + { + path: 'csat', + name: 'csat_reports', + roles: ['administrator'], + component: CsatResponses, + }, + ], + }, ], }; diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js index 2dcf1f9e3..fa5678eee 100755 --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -4,11 +4,12 @@ import Vuex from 'vuex'; import accounts from './modules/accounts'; import agents from './modules/agents'; import auth from './modules/auth'; +import campaigns from './modules/campaigns'; import cannedResponse from './modules/cannedResponse'; import contactConversations from './modules/contactConversations'; -import contacts from './modules/contacts'; import contactLabels from './modules/contactLabels'; -import notifications from './modules/notifications'; +import contactNotes from './modules/contactNotes'; +import contacts from './modules/contacts'; import conversationLabels from './modules/conversationLabels'; import conversationMetadata from './modules/conversationMetadata'; import conversationPage from './modules/conversationPage'; @@ -16,19 +17,19 @@ import conversations from './modules/conversations'; import conversationSearch from './modules/conversationSearch'; import conversationStats from './modules/conversationStats'; import conversationTypingStatus from './modules/conversationTypingStatus'; +import csat from './modules/csat'; import globalConfig from 'shared/store/globalConfig'; +import inboxAssignableAgents from './modules/inboxAssignableAgents'; import inboxes from './modules/inboxes'; import inboxMembers from './modules/inboxMembers'; -import inboxAssignableAgents from './modules/inboxAssignableAgents'; import integrations from './modules/integrations'; import labels from './modules/labels'; +import notifications from './modules/notifications'; import reports from './modules/reports'; +import teamMembers from './modules/teamMembers'; +import teams from './modules/teams'; import userNotificationSettings from './modules/userNotificationSettings'; import webhooks from './modules/webhooks'; -import teams from './modules/teams'; -import teamMembers from './modules/teamMembers'; -import campaigns from './modules/campaigns'; -import contactNotes from './modules/contactNotes'; Vue.use(Vuex); export default new Vuex.Store({ @@ -36,11 +37,12 @@ export default new Vuex.Store({ accounts, agents, auth, + campaigns, cannedResponse, contactConversations, - contacts, contactLabels, - notifications, + contactNotes, + contacts, conversationLabels, conversationMetadata, conversationPage, @@ -48,18 +50,18 @@ export default new Vuex.Store({ conversationSearch, conversationStats, conversationTypingStatus, + csat, globalConfig, + inboxAssignableAgents, inboxes, inboxMembers, - inboxAssignableAgents, integrations, labels, + notifications, reports, + teamMembers, + teams, userNotificationSettings, webhooks, - teams, - teamMembers, - campaigns, - contactNotes, }, }); diff --git a/app/javascript/dashboard/store/modules/csat.js b/app/javascript/dashboard/store/modules/csat.js new file mode 100644 index 000000000..4515dd9c3 --- /dev/null +++ b/app/javascript/dashboard/store/modules/csat.js @@ -0,0 +1,144 @@ +import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; +import types from '../mutation-types'; +import CSATReports from '../../api/csatReports'; + +const computeDistribution = (value, total) => + ((value * 100) / total).toFixed(2); + +export const state = { + records: [], + metrics: { + totalResponseCount: 0, + ratingsCount: { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + }, + totalSentMessagesCount: 0, + }, + uiFlags: { + isFetching: false, + isFetchingMetrics: false, + }, +}; + +export const getters = { + getCSATResponses(_state) { + return _state.records; + }, + getMetrics(_state) { + return _state.metrics; + }, + getUIFlags(_state) { + return _state.uiFlags; + }, + getSatisfactionScore(_state) { + if (!_state.metrics.totalResponseCount) { + return 0; + } + return computeDistribution( + _state.metrics.ratingsCount[4] + _state.metrics.ratingsCount[5], + _state.metrics.totalResponseCount + ); + }, + getResponseRate(_state) { + if (!_state.metrics.totalSentMessagesCount) { + return 0; + } + return computeDistribution( + _state.metrics.totalResponseCount, + _state.metrics.totalSentMessagesCount + ); + }, + getRatingPercentage(_state) { + if (!_state.metrics.totalResponseCount) { + return { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; + } + return { + 1: computeDistribution( + _state.metrics.ratingsCount[1], + _state.metrics.totalResponseCount + ), + 2: computeDistribution( + _state.metrics.ratingsCount[2], + _state.metrics.totalResponseCount + ), + 3: computeDistribution( + _state.metrics.ratingsCount[3], + _state.metrics.totalResponseCount + ), + 4: computeDistribution( + _state.metrics.ratingsCount[4], + _state.metrics.totalResponseCount + ), + 5: computeDistribution( + _state.metrics.ratingsCount[5], + _state.metrics.totalResponseCount + ), + }; + }, +}; + +export const actions = { + get: async function getResponses({ commit }, { page = 1 } = {}) { + commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: true }); + try { + const response = await CSATReports.get({ page }); + commit(types.SET_CSAT_RESPONSE, response.data); + } catch (error) { + // Ignore error + } finally { + commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: false }); + } + }, + getMetrics: async function getMetrics({ commit }) { + commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: true }); + try { + const response = await CSATReports.getMetrics(); + commit(types.SET_CSAT_RESPONSE_METRICS, response.data); + } catch (error) { + // Ignore error + } finally { + commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: false }); + } + }, +}; + +export const mutations = { + [types.SET_CSAT_RESPONSE_UI_FLAG](_state, data) { + _state.uiFlags = { + ..._state.uiFlags, + ...data, + }; + }, + + [types.SET_CSAT_RESPONSE]: MutationHelpers.set, + [types.SET_CSAT_RESPONSE_METRICS]( + _state, + { + total_count: totalResponseCount, + ratings_count: ratingsCount, + total_sent_messages_count: totalSentMessagesCount, + } + ) { + _state.metrics.totalResponseCount = totalResponseCount || 0; + _state.metrics.ratingsCount = { + 1: ratingsCount['1'] || 0, + 2: ratingsCount['2'] || 0, + 3: ratingsCount['3'] || 0, + 4: ratingsCount['4'] || 0, + 5: ratingsCount['5'] || 0, + }; + _state.metrics.totalSentMessagesCount = totalSentMessagesCount || 0; + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/specs/csat/actions.spec.js b/app/javascript/dashboard/store/modules/specs/csat/actions.spec.js new file mode 100644 index 000000000..475559b34 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/csat/actions.spec.js @@ -0,0 +1,64 @@ +import axios from 'axios'; +import { actions } from '../../csat'; +import types from '../../../mutation-types'; + +const commit = jest.fn(); +global.axios = axios; +jest.mock('axios'); + +describe('#actions', () => { + describe('#get', () => { + it('sends correct mutations if API is success', async () => { + axios.get.mockResolvedValue({ + data: [{ id: 1, rating: 1, feedback_text: 'Bad' }], + }); + await actions.get({ commit }, { page: 1 }); + expect(commit.mock.calls).toEqual([ + [types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: true }], + [types.SET_CSAT_RESPONSE, [{ id: 1, rating: 1, feedback_text: 'Bad' }]], + [types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Incorrect header' }); + await actions.get({ commit }, { page: 1 }); + expect(commit.mock.calls).toEqual([ + [types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: true }], + [types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: false }], + ]); + }); + }); + + describe('#getMetrics', () => { + it('sends correct mutations if API is success', async () => { + axios.get.mockResolvedValue({ + data: { + total_count: 29, + ratings_count: { 1: 10, 2: 10, 3: 3, 4: 3, 5: 3 }, + total_sent_messages_count: 120, + }, + }); + await actions.getMetrics({ commit }, { page: 1 }); + expect(commit.mock.calls).toEqual([ + [types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: true }], + [ + types.SET_CSAT_RESPONSE_METRICS, + { + total_count: 29, + ratings_count: { 1: 10, 2: 10, 3: 3, 4: 3, 5: 3 }, + total_sent_messages_count: 120, + }, + ], + [types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Incorrect header' }); + await actions.getMetrics({ commit }, { page: 1 }); + expect(commit.mock.calls).toEqual([ + [types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: true }], + [types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: false }], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/csat/getters.spec.js b/app/javascript/dashboard/store/modules/specs/csat/getters.spec.js new file mode 100644 index 000000000..5a9057b70 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/csat/getters.spec.js @@ -0,0 +1,89 @@ +import { getters } from '../../csat'; + +describe('#getters', () => { + it('getUIFlags', () => { + const state = { uiFlags: { isFetching: false } }; + expect(getters.getUIFlags(state)).toEqual({ isFetching: false }); + }); + + it('getCSATResponses', () => { + const state = { records: [{ id: 1, raring: 1, feedback_text: 'Bad' }] }; + expect(getters.getCSATResponses(state)).toEqual([ + { id: 1, raring: 1, feedback_text: 'Bad' }, + ]); + }); + + it('getMetrics', () => { + const state = { + metrics: { + totalResponseCount: 0, + ratingsCount: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }, + }, + }; + expect(getters.getMetrics(state)).toEqual(state.metrics); + }); + + it('getRatingPercentage', () => { + let state = { + metrics: { + totalResponseCount: 0, + ratingsCount: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }, + }, + }; + expect(getters.getRatingPercentage(state)).toEqual({ + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + }); + + state = { + metrics: { + totalResponseCount: 50, + ratingsCount: { 1: 10, 2: 20, 3: 15, 4: 3, 5: 2 }, + }, + }; + expect(getters.getRatingPercentage(state)).toEqual({ + 1: '20.00', + 2: '40.00', + 3: '30.00', + 4: '6.00', + 5: '4.00', + }); + }); + + it('getResponseRate', () => { + expect( + getters.getResponseRate({ + metrics: { totalResponseCount: 0, totalSentMessagesCount: 0 }, + }) + ).toEqual(0); + + expect( + getters.getResponseRate({ + metrics: { totalResponseCount: 20, totalSentMessagesCount: 50 }, + }) + ).toEqual('40.00'); + }); + + it('getSatisfactionScore', () => { + expect( + getters.getSatisfactionScore({ + metrics: { + totalResponseCount: 0, + ratingsCount: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }, + }, + }) + ).toEqual(0); + + expect( + getters.getSatisfactionScore({ + metrics: { + totalResponseCount: 54, + ratingsCount: { 1: 0, 2: 0, 3: 0, 4: 12, 5: 15 }, + }, + }) + ).toEqual('50.00'); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/csat/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/csat/mutations.spec.js new file mode 100644 index 000000000..421649fec --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/csat/mutations.spec.js @@ -0,0 +1,52 @@ +import types from '../../../mutation-types'; +import { mutations } from '../../csat'; + +describe('#mutations', () => { + describe('#SET_CSAT_RESPONSE_UI_FLAG', () => { + it('set uiFlags correctly', () => { + const state = { uiFlags: { isFetching: true } }; + mutations[types.SET_CSAT_RESPONSE_UI_FLAG](state, { isFetching: false }); + expect(state.uiFlags).toEqual({ isFetching: false }); + }); + }); + + describe('#SET_CSAT_RESPONSE', () => { + it('set records correctly', () => { + const state = { records: [] }; + mutations[types.SET_CSAT_RESPONSE](state, [ + { id: 1, rating: 1, feedback_text: 'Bad' }, + ]); + expect(state.records).toEqual([ + { id: 1, rating: 1, feedback_text: 'Bad' }, + ]); + }); + }); + + describe('#SET_CSAT_RESPONSE_METRICS', () => { + it('set metrics correctly', () => { + const state = { metrics: {} }; + mutations[types.SET_CSAT_RESPONSE_METRICS](state, { + total_count: 29, + ratings_count: { 1: 10, 2: 10, 3: 3, 4: 3, 5: 3 }, + total_sent_messages_count: 120, + }); + expect(state.metrics).toEqual({ + totalResponseCount: 29, + ratingsCount: { 1: 10, 2: 10, 3: 3, 4: 3, 5: 3 }, + totalSentMessagesCount: 120, + }); + }); + + it('set ratingsCount correctly', () => { + const state = { metrics: {} }; + mutations[types.SET_CSAT_RESPONSE_METRICS](state, { + ratings_count: { 1: 5 }, + }); + expect(state.metrics).toEqual({ + totalResponseCount: 0, + ratingsCount: { 1: 5, 2: 0, 3: 0, 4: 0, 5: 0 }, + totalSentMessagesCount: 0, + }); + }); + }); +}); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index a61b14362..df0f8fff6 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -167,4 +167,9 @@ export default { ADD_CONTACT_NOTE: 'ADD_CONTACT_NOTE', EDIT_CONTACT_NOTE: 'EDIT_CONTACT_NOTE', DELETE_CONTACT_NOTE: 'DELETE_CONTACT_NOTE', + + // CSAT Responses + SET_CSAT_RESPONSE_UI_FLAG: 'SET_CSAT_RESPONSE_UI_FLAG', + SET_CSAT_RESPONSE: 'SET_CSAT_RESPONSE', + SET_CSAT_RESPONSE_METRICS: 'SET_CSAT_RESPONSE_METRICS', }; diff --git a/app/javascript/shared/constants/messages.js b/app/javascript/shared/constants/messages.js index 49f6a83cf..f9d7cb332 100644 --- a/app/javascript/shared/constants/messages.js +++ b/app/javascript/shared/constants/messages.js @@ -18,25 +18,30 @@ export const CSAT_RATINGS = [ key: 'disappointed', emoji: '😞', value: 1, + color: '#FDAD2A', }, { key: 'expressionless', emoji: '😑', value: 2, + color: '#FFC532', }, { key: 'neutral', emoji: '😐', value: 3, + color: '#FCEC56', }, { key: 'grinning', emoji: '😀', value: 4, + color: '#6FD86F', }, { key: 'smiling', emoji: '😍', value: 5, + color: '#44CE4B', }, ]; diff --git a/app/models/message.rb b/app/models/message.rb index a3657ce8b..0f2ec21c5 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -86,7 +86,7 @@ class Message < ApplicationRecord after_create_commit :execute_after_create_commit_callbacks - after_update :dispatch_update_event + after_update_commit :dispatch_update_event def channel_token @token ||= inbox.channel.try(:page_access_token) diff --git a/app/policies/csat_survey_response_policy.rb b/app/policies/csat_survey_response_policy.rb index c49b284f0..c0ce8821b 100644 --- a/app/policies/csat_survey_response_policy.rb +++ b/app/policies/csat_survey_response_policy.rb @@ -2,4 +2,8 @@ class CsatSurveyResponsePolicy < ApplicationPolicy def index? @account_user.administrator? end + + def metrics? + @account_user.administrator? + end end diff --git a/app/views/api/v1/accounts/csat_survey_responses/metrics.json.jbuilder b/app/views/api/v1/accounts/csat_survey_responses/metrics.json.jbuilder new file mode 100644 index 000000000..4de47081d --- /dev/null +++ b/app/views/api/v1/accounts/csat_survey_responses/metrics.json.jbuilder @@ -0,0 +1,3 @@ +json.total_count @total_count +json.ratings_count @ratings_count +json.total_sent_messages_count @total_sent_messages_count diff --git a/app/views/api/v1/models/_csat_survey_response.json.jbuilder b/app/views/api/v1/models/_csat_survey_response.json.jbuilder index 3fd60addb..30264e1b5 100644 --- a/app/views/api/v1/models/_csat_survey_response.json.jbuilder +++ b/app/views/api/v1/models/_csat_survey_response.json.jbuilder @@ -3,7 +3,15 @@ json.rating resource.rating json.feedback_message resource.feedback_message json.account_id resource.account_id json.message_id resource.message_id -json.contact resource.contact +if resource.contact + json.contact do + json.partial! 'api/v1/models/contact.json.jbuilder', resource: resource.contact + end +end json.conversation_id resource.conversation.display_id -json.assigned_agent resource.assigned_agent +if resource.assigned_agent + json.assigned_agent do + json.partial! 'api/v1/models/agent.json.jbuilder', resource: resource.assigned_agent + end +end json.created_at resource.created_at diff --git a/config/routes.rb b/config/routes.rb index e8494a0ce..be7d26eb2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -87,7 +87,11 @@ Rails.application.routes.draw do resources :labels, only: [:create, :index] end end - resources :csat_survey_responses, only: [:index] + resources :csat_survey_responses, only: [:index] do + collection do + get :metrics + end + end resources :custom_filters, only: [:index, :show, :create, :update, :destroy] resources :inboxes, only: [:index, :create, :update, :destroy] do get :assignable_agents, on: :member diff --git a/package.json b/package.json index 8f08e1167..c2e6d4021 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "vue-chartjs": "3.5.1", "vue-clickaway": "~2.1.0", "vue-color": "2.8.1", - "vue-easytable": "2.5.1", + "vue-easytable": "2.5.5", "vue-i18n": "8.24.3", "vue-loader": "15.9.6", "vue-multiselect": "~2.1.6", diff --git a/spec/controllers/api/v1/accounts/csat_survey_responses_controller_spec.rb b/spec/controllers/api/v1/accounts/csat_survey_responses_controller_spec.rb index 23e7e96cd..3946f20c5 100644 --- a/spec/controllers/api/v1/accounts/csat_survey_responses_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/csat_survey_responses_controller_spec.rb @@ -33,7 +33,7 @@ RSpec.describe 'CSAT Survey Responses API', type: :request do expect(JSON.parse(response.body).first['feedback_message']).to eq(csat_survey_response.feedback_message) end - it 'filters csat responsed based on a date range' do + it 'filters csat responses based on a date range' do csat_10_days_ago = create(:csat_survey_response, account: account, created_at: 10.days.ago) csat_3_days_ago = create(:csat_survey_response, account: account, created_at: 3.days.ago) @@ -49,4 +49,52 @@ RSpec.describe 'CSAT Survey Responses API', type: :request do end end end + + describe 'GET /api/v1/accounts/{account.id}/csat_survey_responses/metrics' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/csat_survey_responses/metrics" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'returns unauthorized for agents' do + get "/api/v1/accounts/#{account.id}/csat_survey_responses/metrics", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + + it 'returns csat metrics for administrators' do + get "/api/v1/accounts/#{account.id}/csat_survey_responses/metrics", + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + response_data = JSON.parse(response.body) + expect(response_data['total_count']).to eq 1 + expect(response_data['total_sent_messages_count']).to eq 0 + expect(response_data['ratings_count']).to eq({ '1' => 1 }) + end + + it 'filters csat metrics based on a date range' do + create(:csat_survey_response, account: account, created_at: 10.days.ago) + create(:csat_survey_response, account: account, created_at: 3.days.ago) + + get "/api/v1/accounts/#{account.id}/csat_survey_responses/metrics", + params: { since: 5.days.ago.to_time.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s }, + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + response_data = JSON.parse(response.body) + expect(response_data['total_count']).to eq 1 + expect(response_data['total_sent_messages_count']).to eq 0 + expect(response_data['ratings_count']).to eq({ '1' => 1 }) + end + end + end end diff --git a/yarn.lock b/yarn.lock index abebaf297..fad284227 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14966,10 +14966,10 @@ vue-docgen-loader@^1.5.0: loader-utils "^1.2.3" querystring "^0.2.0" -vue-easytable@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/vue-easytable/-/vue-easytable-2.5.1.tgz#2d27a9926edbf79d766465f890f4c75a9848c630" - integrity sha512-HyqcwgV828eQ0DmmHTFedZznjPkS6T2AQHPGsUS59KsHBHs0ZDb7nNs5o+9fOsbYbWt/BkG9B96u2p5iG6Hy5g== +vue-easytable@2.5.5: + version "2.5.5" + resolved "https://registry.yarnpkg.com/vue-easytable/-/vue-easytable-2.5.5.tgz#0d0ac244beb853859c76191c117311b5cf9654b5" + integrity sha512-2dp2+OWYgCn8+g6p+7tNoCuU85OGHqk1hnQ7QnS4HUyPFVAN1n4mmG+6WkIuRKrc5wsFpsg7ZiFxHQnplos3jw== dependencies: lodash "^4.17.20" resize-observer-polyfill "^1.5.1"