From 47f04ee3fec88e2b839b6019f226e1fb533d644c Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Tue, 17 May 2022 21:01:45 +0530 Subject: [PATCH] chore: Add an option to download CSAT Reports (#4694) --- .../csat_survey_responses_controller.rb | 8 +++- app/javascript/dashboard/api/csatReports.js | 11 ++++++ .../dashboard/api/specs/csatReports.spec.js | 18 +++++++++ .../{downloadCsvFile.js => downloadHelper.js} | 6 +++ ...CsvFile.spec.js => downloadHelper.spec.js} | 10 ++++- .../dashboard/i18n/locale/en/report.json | 1 + .../settings/reports/CsatResponses.vue | 27 +++++++++++-- .../reports/components/FilterSelector.vue | 9 ++++- .../reports/components/WootReports.vue | 32 ++++++---------- .../dashboard/store/modules/csat.js | 6 +++ .../dashboard/store/modules/reports.js | 2 +- app/policies/csat_survey_response_policy.rb | 4 ++ .../csat_survey_responses/download.csv.erb | 38 +++++++++++++++++++ config/locales/en.yml | 10 +++++ config/routes.rb | 1 + .../csat_survey_responses_controller_spec.rb | 34 +++++++++++++++++ 16 files changed, 189 insertions(+), 28 deletions(-) rename app/javascript/dashboard/helper/{downloadCsvFile.js => downloadHelper.js} (51%) rename app/javascript/dashboard/helper/specs/{downloadCsvFile.spec.js => downloadHelper.spec.js} (68%) create mode 100644 app/views/api/v1/accounts/csat_survey_responses/download.csv.erb 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 347f028f7..7b5c51d6e 100644 --- a/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb +++ b/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base RESULTS_PER_PAGE = 25 before_action :check_authorization - before_action :set_csat_survey_responses, only: [:index, :metrics] + before_action :set_csat_survey_responses, only: [:index, :metrics, :download] before_action :set_current_page, only: [:index] before_action :set_current_page_surveys, only: [:index] before_action :set_total_sent_messages_count, only: [:metrics] @@ -19,6 +19,12 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base @ratings_count = @csat_survey_responses.group(:rating).count end + def download + response.headers['Content-Type'] = 'text/csv' + response.headers['Content-Disposition'] = 'attachment; filename=csat_report.csv' + render layout: false, template: 'api/v1/accounts/csat_survey_responses/download.csv.erb', format: 'csv' + end + private def set_total_sent_messages_count diff --git a/app/javascript/dashboard/api/csatReports.js b/app/javascript/dashboard/api/csatReports.js index cc0271d7d..4decc8de3 100644 --- a/app/javascript/dashboard/api/csatReports.js +++ b/app/javascript/dashboard/api/csatReports.js @@ -18,6 +18,17 @@ class CSATReportsAPI extends ApiClient { }); } + download({ from, to, user_ids } = {}) { + return axios.get(`${this.url}/download`, { + params: { + since: from, + until: to, + sort: '-created_at', + user_ids, + }, + }); + } + getMetrics({ from, to, user_ids } = {}) { return axios.get(`${this.url}/metrics`, { params: { since: from, until: to, user_ids }, diff --git a/app/javascript/dashboard/api/specs/csatReports.spec.js b/app/javascript/dashboard/api/specs/csatReports.spec.js index 0022a91ad..a1d6e50f2 100644 --- a/app/javascript/dashboard/api/specs/csatReports.spec.js +++ b/app/javascript/dashboard/api/specs/csatReports.spec.js @@ -33,5 +33,23 @@ describe('#Reports API', () => { } ); }); + it('#download', () => { + csatReportsAPI.download({ + from: 1622485800, + to: 1623695400, + user_ids: 1, + }); + expect(context.axiosMock.get).toHaveBeenCalledWith( + '/api/v1/csat_survey_responses/download', + { + params: { + since: 1622485800, + until: 1623695400, + user_ids: 1, + sort: '-created_at', + }, + } + ); + }); }); }); diff --git a/app/javascript/dashboard/helper/downloadCsvFile.js b/app/javascript/dashboard/helper/downloadHelper.js similarity index 51% rename from app/javascript/dashboard/helper/downloadCsvFile.js rename to app/javascript/dashboard/helper/downloadHelper.js index f0a13a1fd..150ac1fd8 100644 --- a/app/javascript/dashboard/helper/downloadCsvFile.js +++ b/app/javascript/dashboard/helper/downloadHelper.js @@ -1,6 +1,12 @@ +import fromUnixTime from 'date-fns/fromUnixTime'; +import format from 'date-fns/format'; + 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(); }; + +export const generateFileName = ({ type, to }) => + `${type}-report-${format(fromUnixTime(to), 'dd-MM-yyyy')}.csv`; diff --git a/app/javascript/dashboard/helper/specs/downloadCsvFile.spec.js b/app/javascript/dashboard/helper/specs/downloadHelper.spec.js similarity index 68% rename from app/javascript/dashboard/helper/specs/downloadCsvFile.spec.js rename to app/javascript/dashboard/helper/specs/downloadHelper.spec.js index d05b0a841..b294dfe16 100644 --- a/app/javascript/dashboard/helper/specs/downloadCsvFile.spec.js +++ b/app/javascript/dashboard/helper/specs/downloadHelper.spec.js @@ -1,4 +1,4 @@ -import { downloadCsvFile } from '../downloadCsvFile'; +import { downloadCsvFile, generateFileName } from '../downloadHelper'; const fileName = 'test.csv'; const fileData = `Agent name,Conversations count,Avg first response time (Minutes),Avg resolution time (Minutes) @@ -19,3 +19,11 @@ describe('#downloadCsvFile', () => { expect(link.click).toHaveBeenCalledTimes(1); }); }); + +describe('#generateFileName', () => { + it('should generate the correct file name', () => { + expect(generateFileName({ type: 'csat', to: 1652812199 })).toEqual( + 'csat-report-17-05-2022.csv' + ); + }); +}); diff --git a/app/javascript/dashboard/i18n/locale/en/report.json b/app/javascript/dashboard/i18n/locale/en/report.json index 35a349b56..5f2c6ae48 100644 --- a/app/javascript/dashboard/i18n/locale/en/report.json +++ b/app/javascript/dashboard/i18n/locale/en/report.json @@ -354,6 +354,7 @@ "CSAT_REPORTS": { "HEADER": "CSAT Reports", "NO_RECORDS": "There are no CSAT survey responses available.", + "DOWNLOAD": "Download CSAT Reports", "FILTERS": { "AGENTS": { "PLACEHOLDER": "Choose Agents" diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/CsatResponses.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/CsatResponses.vue index 4498ead8a..735c173ef 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/CsatResponses.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/CsatResponses.vue @@ -3,9 +3,18 @@ + + {{ $t('CSAT_REPORTS.DOWNLOAD') }} + @@ -15,6 +24,7 @@ import CsatMetrics from './components/CsatMetrics'; import CsatTable from './components/CsatTable'; import ReportFilterSelector from './components/FilterSelector'; import { mapGetters } from 'vuex'; +import { generateFileName } from '../../../../helper/downloadHelper'; export default { name: 'CsatResponses', @@ -24,7 +34,7 @@ export default { ReportFilterSelector, }, data() { - return { pageIndex: 1, from: 0, to: 0, user_ids: [] }; + return { pageIndex: 1, from: 0, to: 0, userIds: [] }; }, computed: { ...mapGetters({ @@ -39,7 +49,7 @@ export default { this.$store.dispatch('csat/getMetrics', { from: this.from, to: this.to, - user_ids: this.user_ids, + user_ids: this.userIds, }); this.getResponses(); }, @@ -48,7 +58,7 @@ export default { page: this.pageIndex, from: this.from, to: this.to, - user_ids: this.user_ids, + user_ids: this.userIds, }); }, onPageNumberChange(pageIndex) { @@ -61,9 +71,18 @@ export default { this.getAllData(); }, onAgentsFilterChange(agents) { - this.user_ids = agents.map(el => el.id); + this.userIds = agents.map(el => el.id); this.getAllData(); }, + downloadReports() { + const type = 'csat'; + this.$store.dispatch('csat/downloadCSATReports', { + from: this.from, + to: this.to, + user_ids: this.userIds, + fileName: generateFileName({ type, to: this.to }), + }); + }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue index cadf4078e..848555a4d 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue @@ -61,7 +61,10 @@ @input="handleAgentsFilterSelection" /> -
+
{{ $t('REPORT.BUSINESS_HOURS') }} @@ -105,6 +108,10 @@ export default { type: Boolean, default: false, }, + showBusinessHoursSwitch: { + type: Boolean, + default: true, + }, }, data() { return { 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 6d8b49c62..34188a12a 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue @@ -61,6 +61,7 @@ import format from 'date-fns/format'; import { GROUP_BY_FILTER, METRIC_CHART } from '../constants'; import reportMixin from '../../../../../mixins/reportMixin'; import { formatTime } from '@chatwoot/utils'; +import { generateFileName } from '../../../../../helper/downloadHelper'; const REPORTS_KEYS = { CONVERSATIONS: 'conversations_count', @@ -250,26 +251,17 @@ export default { }); }, downloadReports() { - const { from, to } = this; - const fileName = `${this.type}-report-${format( - fromUnixTime(to), - 'dd-MM-yyyy' - )}.csv`; - switch (this.type) { - case 'agent': - this.$store.dispatch('downloadAgentReports', { from, to, fileName }); - break; - case 'label': - this.$store.dispatch('downloadLabelReports', { from, to, fileName }); - break; - case 'inbox': - this.$store.dispatch('downloadInboxReports', { from, to, fileName }); - break; - case 'team': - this.$store.dispatch('downloadTeamReports', { from, to, fileName }); - break; - default: - break; + const { from, to, type } = this; + const dispatchMethods = { + agent: 'downloadAgentReports', + label: 'downloadLabelReports', + inbox: 'downloadInboxReports', + team: 'downloadTeamReports', + }; + if (dispatchMethods[type]) { + const fileName = generateFileName({ type, to }); + const params = { from, to, fileName }; + this.$store.dispatch(dispatchMethods[type], params); } }, changeSelection(index) { diff --git a/app/javascript/dashboard/store/modules/csat.js b/app/javascript/dashboard/store/modules/csat.js index 7bc1dad6d..2557e2698 100644 --- a/app/javascript/dashboard/store/modules/csat.js +++ b/app/javascript/dashboard/store/modules/csat.js @@ -1,6 +1,7 @@ import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; import types from '../mutation-types'; import CSATReports from '../../api/csatReports'; +import { downloadCsvFile } from '../../helper/downloadHelper'; const computeDistribution = (value, total) => ((value * 100) / total).toFixed(2); @@ -107,6 +108,11 @@ export const actions = { commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: false }); } }, + downloadCSATReports(_, params) { + return CSATReports.download(params).then(response => { + downloadCsvFile(params.fileName, response.data); + }); + }, }; export const mutations = { diff --git a/app/javascript/dashboard/store/modules/reports.js b/app/javascript/dashboard/store/modules/reports.js index dc9a6c33b..9ae571803 100644 --- a/app/javascript/dashboard/store/modules/reports.js +++ b/app/javascript/dashboard/store/modules/reports.js @@ -5,7 +5,7 @@ import * as types from '../mutation-types'; import Report from '../../api/reports'; import Vue from 'vue'; -import { downloadCsvFile } from '../../helper/downloadCsvFile'; +import { downloadCsvFile } from '../../helper/downloadHelper'; const state = { fetchingStatus: false, diff --git a/app/policies/csat_survey_response_policy.rb b/app/policies/csat_survey_response_policy.rb index c0ce8821b..afcce00e9 100644 --- a/app/policies/csat_survey_response_policy.rb +++ b/app/policies/csat_survey_response_policy.rb @@ -6,4 +6,8 @@ class CsatSurveyResponsePolicy < ApplicationPolicy def metrics? @account_user.administrator? end + + def download? + @account_user.administrator? + end end diff --git a/app/views/api/v1/accounts/csat_survey_responses/download.csv.erb b/app/views/api/v1/accounts/csat_survey_responses/download.csv.erb new file mode 100644 index 000000000..8b694ed35 --- /dev/null +++ b/app/views/api/v1/accounts/csat_survey_responses/download.csv.erb @@ -0,0 +1,38 @@ +<%= + CSV.generate_line([ + I18n.t('reports.csat.headers.agent_name'), + I18n.t('reports.csat.headers.rating'), + I18n.t('reports.csat.headers.feedback'), + I18n.t('reports.csat.headers.contact_name'), + I18n.t('reports.csat.headers.contact_email_address'), + I18n.t('reports.csat.headers.contact_phone_number'), + I18n.t('reports.csat.headers.link_to_the_conversation'), + I18n.t('reports.csat.headers.recorded_at') + ]) +-%> +<% @csat_survey_responses.each do |csat_response| %> +<% assigned_agent = csat_response.assigned_agent %> +<% contact = csat_response.contact %> +<% conversation = csat_response.conversation %> +<%= + CSV.generate_line([ + assigned_agent ? "#{assigned_agent.name} (#{assigned_agent.email})" : nil, + csat_response.rating, + csat_response.feedback_message.present? ? csat_response.feedback_message : nil, + contact&.name.present? ? contact&.name: nil, + contact&.email.present? ? contact&.email: nil, + contact&.phone_number.present? ? contact&.phone_number: nil, + conversation ? app_account_conversation_url(account_id: Current.account.id, id: conversation.display_id): nil, + csat_response.created_at, + ]) +-%> +<% end %> +<%= + CSV.generate_line([ + I18n.t( + 'reports.period', + since: Date.strptime(params[:since], '%s'), + until: Date.strptime(params[:until], '%s') + ) + ]) +-%> diff --git a/config/locales/en.yml b/config/locales/en.yml index 0c92c32eb..bd7829b53 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -60,6 +60,16 @@ en: avg_first_response_time: Avg first response time (Minutes) avg_resolution_time: Avg resolution time (Minutes) default_group_by: day + csat: + headers: + contact_name: Contact Name + contact_email_address: Contact Email Address + contact_phone_number: Contact Phone Number + link_to_the_conversation: Link to the conversation + agent_name: Agent Name + rating: Rating + feedback: Feedback Comment + recorded_at: Recorded date notifications: notification_title: diff --git a/config/routes.rb b/config/routes.rb index aa23be1e7..a80f6ca39 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -106,6 +106,7 @@ Rails.application.routes.draw do resources :csat_survey_responses, only: [:index] do collection do get :metrics + get :download end end resources :custom_attribute_definitions, only: [:index, :show, :create, :update, :destroy] 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 abb4231d9..f431f0ed5 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 @@ -148,4 +148,38 @@ RSpec.describe 'CSAT Survey Responses API', type: :request do end end end + + describe 'GET /api/v1/accounts/{account.id}/csat_survey_responses/download' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/csat_survey_responses/download" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:params) { { since: 5.days.ago.to_time.to_i.to_s, until: Time.zone.tomorrow.to_time.to_i.to_s } } + + it 'returns unauthorized for agents' do + get "/api/v1/accounts/#{account.id}/csat_survey_responses/download", + params: params, + headers: agent.create_new_auth_token + + expect(response).to have_http_status(:unauthorized) + end + + it 'returns summary' do + get "/api/v1/accounts/#{account.id}/csat_survey_responses/download", + params: params, + headers: administrator.create_new_auth_token + + expect(response).to have_http_status(:success) + + content = CSV.parse(response.body) + # Check rating from CSAT Row + expect(content[1][1]).to eq '1' + expect(content.length).to eq 3 + end + end + end end