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 @@
-
+
{{ $t('LABEL_REPORTS.FILTER_DROPDOWN_LABEL') }}
+ />
- {{
- props.option.title
- }}
+
+ {{ props.option.title }}
+
@@ -78,15 +78,15 @@
"
>
- {{
- props.option.title
- }}
+
+ {{ props.option.title }}
+
-
+
{{ $t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL') }}
@@ -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