From 1c6a539c0a2fe853016279251f41d71f16db0215 Mon Sep 17 00:00:00 2001 From: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Date: Wed, 6 Oct 2021 23:53:51 +0530 Subject: [PATCH] feat: Add Reports for teams (#3116) Co-authored-by: Pranav Raj S --- app/builders/v2/report_builder.rb | 5 +- app/javascript/dashboard/api/reports.js | 6 ++ .../dashboard/api/specs/reports.spec.js | 14 +++++ .../dashboard/helper/downloadCsvFile.js | 6 ++ .../helper/specs/downloadCsvFile.spec.js | 21 +++++++ .../dashboard/i18n/locale/en/report.json | 63 +++++++++++++++++++ .../dashboard/i18n/locale/en/settings.json | 3 +- .../dashboard/i18n/sidebarItems/reports.js | 10 ++- .../settings/reports/TeamReports.vue | 19 ++++++ .../reports/components/ReportFilters.vue | 35 ++++++----- .../reports/components/WootReports.vue | 24 +++++-- .../settings/reports/reports.routes.js | 17 +++++ .../dashboard/store/modules/reports.js | 41 +++++------- .../modules/specs/reports/actions.spec.js | 21 +++++++ spec/builders/v2/report_builder_spec.rb | 14 ++--- 15 files changed, 240 insertions(+), 59 deletions(-) create mode 100644 app/javascript/dashboard/helper/downloadCsvFile.js create mode 100644 app/javascript/dashboard/helper/specs/downloadCsvFile.spec.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/TeamReports.vue diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb index 33b2db820..82e6ce94c 100644 --- a/app/builders/v2/report_builder.rb +++ b/app/builders/v2/report_builder.rb @@ -68,15 +68,14 @@ class V2::ReportBuilder .count end - # unscoped removes all scopes added to a model previously def incoming_messages_count - scope.messages.unscoped.where(account_id: account.id).incoming + scope.messages.incoming.unscope(:order) .group_by_day(:created_at, range: range, default_value: 0) .count end def outgoing_messages_count - scope.messages.unscoped.where(account_id: account.id).outgoing + scope.messages.outgoing.unscope(:order) .group_by_day(:created_at, range: range, default_value: 0) .count end diff --git a/app/javascript/dashboard/api/reports.js b/app/javascript/dashboard/api/reports.js index 9abe68405..faeee779f 100644 --- a/app/javascript/dashboard/api/reports.js +++ b/app/javascript/dashboard/api/reports.js @@ -35,6 +35,12 @@ class ReportsAPI extends ApiClient { params: { since, until }, }); } + + getTeamReports(since, until) { + return axios.get(`${this.url}/teams`, { + params: { since, until }, + }); + } } export default new ReportsAPI(); diff --git a/app/javascript/dashboard/api/specs/reports.spec.js b/app/javascript/dashboard/api/specs/reports.spec.js index 81b4d8429..f3b66996a 100644 --- a/app/javascript/dashboard/api/specs/reports.spec.js +++ b/app/javascript/dashboard/api/specs/reports.spec.js @@ -16,6 +16,7 @@ describe('#Reports API', () => { expect(reportsAPI).toHaveProperty('getAgentReports'); expect(reportsAPI).toHaveProperty('getLabelReports'); expect(reportsAPI).toHaveProperty('getInboxReports'); + expect(reportsAPI).toHaveProperty('getTeamReports'); }); describeWithAPIMock('API calls', context => { it('#getAccountReports', () => { @@ -82,5 +83,18 @@ describe('#Reports API', () => { } ); }); + + it('#getTeamReports', () => { + reportsAPI.getTeamReports(1621103400, 1621621800); + expect(context.axiosMock.get).toHaveBeenCalledWith( + '/api/v2/reports/teams', + { + params: { + since: 1621103400, + until: 1621621800, + }, + } + ); + }); }); }); diff --git a/app/javascript/dashboard/helper/downloadCsvFile.js b/app/javascript/dashboard/helper/downloadCsvFile.js new file mode 100644 index 000000000..f0a13a1fd --- /dev/null +++ b/app/javascript/dashboard/helper/downloadCsvFile.js @@ -0,0 +1,6 @@ +export const downloadCsvFile = (fileName, fileContent) => { + const link = document.createElement('a'); + link.download = fileName; + link.href = `data:text/csv;charset=utf-8,` + encodeURI(fileContent); + link.click(); +}; diff --git a/app/javascript/dashboard/helper/specs/downloadCsvFile.spec.js b/app/javascript/dashboard/helper/specs/downloadCsvFile.spec.js new file mode 100644 index 000000000..d05b0a841 --- /dev/null +++ b/app/javascript/dashboard/helper/specs/downloadCsvFile.spec.js @@ -0,0 +1,21 @@ +import { downloadCsvFile } from '../downloadCsvFile'; + +const fileName = 'test.csv'; +const fileData = `Agent name,Conversations count,Avg first response time (Minutes),Avg resolution time (Minutes) +Pranav,36,114,28411`; + +describe('#downloadCsvFile', () => { + it('should download the csv file', () => { + const link = { + click: jest.fn(), + }; + jest.spyOn(document, 'createElement').mockImplementation(() => link); + + downloadCsvFile(fileName, fileData); + expect(link.download).toEqual(fileName); + expect(link.href).toEqual( + `data:text/csv;charset=utf-8,${encodeURI(fileData)}` + ); + expect(link.click).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/javascript/dashboard/i18n/locale/en/report.json b/app/javascript/dashboard/i18n/locale/en/report.json index b1885a2aa..4afd63b61 100644 --- a/app/javascript/dashboard/i18n/locale/en/report.json +++ b/app/javascript/dashboard/i18n/locale/en/report.json @@ -250,6 +250,69 @@ "PLACEHOLDER": "Select date range" } }, + "TEAM_REPORTS": { + "HEADER": "Team Overview", + "LOADING_CHART": "Loading chart data...", + "NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.", + "DOWNLOAD_TEAM_REPORTS": "Download team reports", + "FILTER_DROPDOWN_LABEL": "Select Team", + "METRICS": { + "CONVERSATIONS": { + "NAME": "Conversations", + "DESC": "( Total )" + }, + "INCOMING_MESSAGES": { + "NAME": "Incoming Messages", + "DESC": "( Total )" + }, + "OUTGOING_MESSAGES": { + "NAME": "Outgoing Messages", + "DESC": "( Total )" + }, + "FIRST_RESPONSE_TIME": { + "NAME": "First response time", + "DESC": "( Avg )" + }, + "RESOLUTION_TIME": { + "NAME": "Resolution Time", + "DESC": "( Avg )" + }, + "RESOLUTION_COUNT": { + "NAME": "Resolution Count", + "DESC": "( Total )" + } + }, + "DATE_RANGE": [ + { + "id": 0, + "name": "Last 7 days" + }, + { + "id": 1, + "name": "Last 30 days" + }, + { + "id": 2, + "name": "Last 3 months" + }, + { + "id": 3, + "name": "Last 6 months" + }, + { + "id": 4, + "name": "Last year" + }, + { + "id": 5, + "name": "Custom date range" + } + ], + "CUSTOM_DATE_RANGE": { + "CONFIRM": "Apply", + "PLACEHOLDER": "Select date range" + } + }, "CSAT_REPORTS": { "HEADER": "CSAT Reports", "NO_RECORDS": "There are no CSAT survey responses available.", diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 98ad27840..aae345c88 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -149,7 +149,8 @@ "ONE_OFF": "One off", "REPORTS_AGENT": "Agents", "REPORTS_LABEL": "Labels", - "REPORTS_INBOX": "Inbox" + "REPORTS_INBOX": "Inbox", + "REPORTS_TEAM": "Team" }, "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/i18n/sidebarItems/reports.js b/app/javascript/dashboard/i18n/sidebarItems/reports.js index a37536760..66595d691 100644 --- a/app/javascript/dashboard/i18n/sidebarItems/reports.js +++ b/app/javascript/dashboard/i18n/sidebarItems/reports.js @@ -7,6 +7,7 @@ const reports = accountId => ({ 'agent_reports', 'label_reports', 'inbox_reports', + 'team_reports', ], menuItems: { back: { @@ -31,7 +32,7 @@ const reports = accountId => ({ toStateName: 'csat_reports', }, agentReports: { - icon: 'ion-ios-people', + icon: 'ion-person-stalker', label: 'REPORTS_AGENT', hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/reports/agent`), @@ -51,6 +52,13 @@ const reports = accountId => ({ toState: frontendURL(`accounts/${accountId}/reports/inboxes`), toStateName: 'inbox_reports', }, + teamReports: { + icon: 'ion-ios-people', + label: 'REPORTS_TEAM', + hasSubMenu: false, + toState: frontendURL(`accounts/${accountId}/reports/teams`), + toStateName: 'team_reports', + }, }, }); diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/TeamReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/TeamReports.vue new file mode 100644 index 000000000..f557039cc --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/TeamReports.vue @@ -0,0 +1,19 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue index b60804661..a2f459734 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue @@ -6,7 +6,7 @@

-
+
+ /> - {{ - props.option.title - }} + + {{ props.option.title }} +
@@ -78,15 +78,15 @@ " > - {{ - props.option.title - }} + + {{ props.option.title }} + -
+
@@ -94,7 +94,7 @@ v-model="currentSelectedFilter" track-by="id" label="name" - :placeholder="$t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL')" + :placeholder="multiselectLabel" selected-label :select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')" deselect-label="" @@ -185,12 +185,19 @@ export default { const fromDate = subDays(new Date(), diff); return this.fromCustomDate(fromDate); }, + multiselectLabel() { + const typeLabels = { + agent: this.$t('AGENT_REPORTS.FILTER_DROPDOWN_LABEL'), + label: this.$t('LABEL_REPORTS.FILTER_DROPDOWN_LABEL'), + inbox: this.$t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL'), + team: this.$t('TEAM_REPORTS.FILTER_DROPDOWN_LABEL'), + }; + return typeLabels[this.type] || this.$t('FORMS.MULTISELECT.SELECT_ONE'); + }, }, watch: { filterItemsList(val) { this.currentSelectedFilter = val[0]; - }, - currentSelectedFilter() { this.changeFilterSelection(); }, }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue index 64821f62e..b5d4f889d 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue @@ -15,8 +15,8 @@ @date-range-change="onDateRangeChange" @filter-change="onFilterChange" /> -
-
+
+
- + {{ $t('REPORT.NO_ENOUGH_DATA') }} @@ -118,9 +121,15 @@ export default { }; }, metrics() { - const reportKeys = [ - 'CONVERSATIONS', - 'INCOMING_MESSAGES', + let reportKeys = ['CONVERSATIONS']; + // If report type is agent, we don't need to show + // incoming messages count, as there will not be any message + // sent by an agent which is incoming. + if (this.type !== 'agent') { + reportKeys.push('INCOMING_MESSAGES'); + } + reportKeys = [ + ...reportKeys, 'OUTGOING_MESSAGES', 'FIRST_RESPONSE_TIME', 'RESOLUTION_TIME', @@ -175,6 +184,9 @@ export default { case 'inbox': this.$store.dispatch('downloadInboxReports', { from, to, fileName }); break; + case 'team': + this.$store.dispatch('downloadTeamReports', { from, to, fileName }); + break; default: break; } 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 d37331f02..4cb837af5 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/reports.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/reports.routes.js @@ -2,6 +2,7 @@ import Index from './Index'; import AgentReports from './AgentReports'; import LabelReports from './LabelReports'; import InboxReports from './InboxReports'; +import TeamReports from './TeamReports'; import CsatResponses from './CsatResponses'; import SettingsContent from '../Wrapper'; import { frontendURL } from '../../../../helper/URLHelper'; @@ -97,5 +98,21 @@ export default { }, ], }, + { + path: frontendURL('accounts/:accountId/reports'), + component: SettingsContent, + props: { + headerTitle: 'TEAM_REPORTS.HEADER', + icon: 'ion-ios-people', + }, + children: [ + { + path: 'teams', + name: 'team_reports', + roles: ['administrator'], + component: TeamReports, + }, + ], + }, ], }; diff --git a/app/javascript/dashboard/store/modules/reports.js b/app/javascript/dashboard/store/modules/reports.js index 828e8114f..87050bcef 100644 --- a/app/javascript/dashboard/store/modules/reports.js +++ b/app/javascript/dashboard/store/modules/reports.js @@ -7,6 +7,8 @@ import fromUnixTime from 'date-fns/fromUnixTime'; import * as types from '../mutation-types'; import Report from '../../api/reports'; +import { downloadCsvFile } from '../../helper/downloadCsvFile'; + const state = { fetchingStatus: false, reportData: [], @@ -78,15 +80,7 @@ export const actions = { downloadAgentReports(_, reportObj) { return Report.getAgentReports(reportObj.from, reportObj.to) .then(response => { - let csvContent = 'data:text/csv;charset=utf-8,' + response.data; - var encodedUri = encodeURI(csvContent); - var downloadLink = document.createElement('a'); - downloadLink.href = encodedUri; - downloadLink.download = reportObj.fileName; - - document.body.appendChild(downloadLink); - downloadLink.click(); - document.body.removeChild(downloadLink); + downloadCsvFile(reportObj.fileName, response.data); }) .catch(error => { console.error(error); @@ -95,15 +89,7 @@ export const actions = { downloadLabelReports(_, reportObj) { return Report.getLabelReports(reportObj.from, reportObj.to) .then(response => { - let csvContent = 'data:text/csv;charset=utf-8,' + response.data; - var encodedUri = encodeURI(csvContent); - var downloadLink = document.createElement('a'); - downloadLink.href = encodedUri; - downloadLink.download = reportObj.fileName; - - document.body.appendChild(downloadLink); - downloadLink.click(); - document.body.removeChild(downloadLink); + downloadCsvFile(reportObj.fileName, response.data); }) .catch(error => { console.error(error); @@ -112,15 +98,16 @@ export const actions = { downloadInboxReports(_, reportObj) { return Report.getInboxReports(reportObj.from, reportObj.to) .then(response => { - let csvContent = 'data:text/csv;charset=utf-8,' + response.data; - var encodedUri = encodeURI(csvContent); - var downloadLink = document.createElement('a'); - downloadLink.href = encodedUri; - downloadLink.download = reportObj.fileName; - - document.body.appendChild(downloadLink); - downloadLink.click(); - // document.body.removeChild(downloadLink); + downloadCsvFile(reportObj.fileName, response.data); + }) + .catch(error => { + console.error(error); + }); + }, + downloadTeamReports(_, reportObj) { + return Report.getTeamReports(reportObj.from, reportObj.to) + .then(response => { + downloadCsvFile(reportObj.fileName, response.data); }) .catch(error => { console.error(error); diff --git a/app/javascript/dashboard/store/modules/specs/reports/actions.spec.js b/app/javascript/dashboard/store/modules/specs/reports/actions.spec.js index 10ccbdcd3..0ba89ff81 100644 --- a/app/javascript/dashboard/store/modules/specs/reports/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/reports/actions.spec.js @@ -78,4 +78,25 @@ describe('#actions', () => { expect(mockInboxDownloadElement.download).toEqual(param.fileName); }); }); + + describe('#downloadTeamReports', () => { + it('open CSV download prompt if API is success', async () => { + axios.get.mockResolvedValue({ + data: `Team name,Conversations count,Avg first response time (Minutes),Avg resolution time (Minutes) + sales team,0,0,0 + Reporting period 2021-09-23 to 2021-09-29`, + }); + const param = { + from: 1631039400, + to: 1635013800, + fileName: 'inbox-report-24-10-2021.csv', + }; + const mockInboxDownloadElement = createElementSpy(); + await actions.downloadInboxReports(1, param); + expect(mockInboxDownloadElement.href).toEqual( + 'data:text/csv;charset=utf-8,Team%20name,Conversations%20count,Avg%20first%20response%20time%20(Minutes),Avg%20resolution%20time%20(Minutes)%0A%20%20%20%20%20%20%20%20sales%20team,0,0,0%0A%20%20%20%20%20%20%20%20Reporting%20period%202021-09-23%20to%202021-09-29' + ); + expect(mockInboxDownloadElement.download).toEqual(param.fileName); + }); + }); }); diff --git a/spec/builders/v2/report_builder_spec.rb b/spec/builders/v2/report_builder_spec.rb index 8506c88f6..bcf02f1d8 100644 --- a/spec/builders/v2/report_builder_spec.rb +++ b/spec/builders/v2/report_builder_spec.rb @@ -171,14 +171,14 @@ describe ::V2::ReportBuilder do type: :label, id: label_1.id, since: (Time.zone.today - 3.days).to_time.to_i.to_s, - until: Time.zone.today.to_time.to_i.to_s + until: (Time.zone.today + 1.day).to_time.to_i.to_s } builder = V2::ReportBuilder.new(account, params) metrics = builder.timeseries expect(metrics[Time.zone.today]).to be 20 - expect(metrics[Time.zone.today - 2.days]).to be 5 + expect(metrics[Time.zone.today - 2.days]).to be 0 end it 'return outgoing messages count' do @@ -187,14 +187,14 @@ describe ::V2::ReportBuilder do type: :label, id: label_1.id, since: (Time.zone.today - 3.days).to_time.to_i.to_s, - until: Time.zone.today.to_time.to_i.to_s + until: (Time.zone.today + 1.day).to_time.to_i.to_s } builder = V2::ReportBuilder.new(account, params) metrics = builder.timeseries expect(metrics[Time.zone.today]).to be 50 - expect(metrics[Time.zone.today - 2.days]).to be 15 + expect(metrics[Time.zone.today - 2.days]).to be 0 end it 'return resolutions count' do @@ -203,7 +203,7 @@ describe ::V2::ReportBuilder do type: :label, id: label_2.id, since: (Time.zone.today - 3.days).to_time.to_i.to_s, - until: Time.zone.today.to_time.to_i.to_s + until: (Time.zone.today + 1.day).to_time.to_i.to_s } conversations = account.conversations.where('created_at < ?', 1.day.ago) @@ -242,8 +242,8 @@ describe ::V2::ReportBuilder do metrics = builder.summary expect(metrics[:conversations_count]).to be 5 - expect(metrics[:incoming_messages_count]).to be 25 - expect(metrics[:outgoing_messages_count]).to be 65 + expect(metrics[:incoming_messages_count]).to be 5 + expect(metrics[:outgoing_messages_count]).to be 15 expect(metrics[:avg_resolution_time]).to be 0 expect(metrics[:resolutions_count]).to be 0 end