mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	chore: Add an option to download CSAT Reports (#4694)
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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 }, | ||||
|   | ||||
| @@ -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', | ||||
|           }, | ||||
|         } | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -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`; | ||||
| @@ -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' | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
| @@ -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" | ||||
|   | ||||
| @@ -3,9 +3,18 @@ | ||||
|     <report-filter-selector | ||||
|       agents-filter | ||||
|       :agents-filter-items-list="agentList" | ||||
|       :show-business-hours-switch="false" | ||||
|       @date-range-change="onDateRangeChange" | ||||
|       @agents-filter-change="onAgentsFilterChange" | ||||
|     /> | ||||
|     <woot-button | ||||
|       color-scheme="success" | ||||
|       class-names="button--fixed-right-top" | ||||
|       icon="arrow-download" | ||||
|       @click="downloadReports" | ||||
|     > | ||||
|       {{ $t('CSAT_REPORTS.DOWNLOAD') }} | ||||
|     </woot-button> | ||||
|     <csat-metrics /> | ||||
|     <csat-table :page-index="pageIndex" @page-change="onPageNumberChange" /> | ||||
|   </div> | ||||
| @@ -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 }), | ||||
|       }); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|   | ||||
| @@ -61,7 +61,10 @@ | ||||
|         @input="handleAgentsFilterSelection" | ||||
|       /> | ||||
|     </div> | ||||
|     <div class="small-12 medium-3 business-hours"> | ||||
|     <div | ||||
|       v-if="showBusinessHoursSwitch" | ||||
|       class="small-12 medium-3 business-hours" | ||||
|     > | ||||
|       <span class="business-hours-text margin-right-small"> | ||||
|         {{ $t('REPORT.BUSINESS_HOURS') }} | ||||
|       </span> | ||||
| @@ -105,6 +108,10 @@ export default { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|     showBusinessHoursSwitch: { | ||||
|       type: Boolean, | ||||
|       default: true, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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 = { | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -6,4 +6,8 @@ class CsatSurveyResponsePolicy < ApplicationPolicy | ||||
|   def metrics? | ||||
|     @account_user.administrator? | ||||
|   end | ||||
|  | ||||
|   def download? | ||||
|     @account_user.administrator? | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -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') | ||||
|     ) | ||||
|   ]) | ||||
| -%> | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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] | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Pranav Raj S
					Pranav Raj S