chore: Add an option to download CSAT Reports (#4694)

This commit is contained in:
Pranav Raj S 2022-05-17 21:01:45 +05:30 committed by GitHub
parent 361ffbab82
commit 47f04ee3fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 189 additions and 28 deletions

View file

@ -5,7 +5,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
RESULTS_PER_PAGE = 25 RESULTS_PER_PAGE = 25
before_action :check_authorization 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, only: [:index]
before_action :set_current_page_surveys, only: [:index] before_action :set_current_page_surveys, only: [:index]
before_action :set_total_sent_messages_count, only: [:metrics] 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 @ratings_count = @csat_survey_responses.group(:rating).count
end 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 private
def set_total_sent_messages_count def set_total_sent_messages_count

View file

@ -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 } = {}) { getMetrics({ from, to, user_ids } = {}) {
return axios.get(`${this.url}/metrics`, { return axios.get(`${this.url}/metrics`, {
params: { since: from, until: to, user_ids }, params: { since: from, until: to, user_ids },

View file

@ -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',
},
}
);
});
}); });
}); });

View file

@ -1,6 +1,12 @@
import fromUnixTime from 'date-fns/fromUnixTime';
import format from 'date-fns/format';
export const downloadCsvFile = (fileName, fileContent) => { export const downloadCsvFile = (fileName, fileContent) => {
const link = document.createElement('a'); const link = document.createElement('a');
link.download = fileName; link.download = fileName;
link.href = `data:text/csv;charset=utf-8,` + encodeURI(fileContent); link.href = `data:text/csv;charset=utf-8,` + encodeURI(fileContent);
link.click(); link.click();
}; };
export const generateFileName = ({ type, to }) =>
`${type}-report-${format(fromUnixTime(to), 'dd-MM-yyyy')}.csv`;

View file

@ -1,4 +1,4 @@
import { downloadCsvFile } from '../downloadCsvFile'; import { downloadCsvFile, generateFileName } from '../downloadHelper';
const fileName = 'test.csv'; const fileName = 'test.csv';
const fileData = `Agent name,Conversations count,Avg first response time (Minutes),Avg resolution time (Minutes) 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); 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'
);
});
});

View file

@ -354,6 +354,7 @@
"CSAT_REPORTS": { "CSAT_REPORTS": {
"HEADER": "CSAT Reports", "HEADER": "CSAT Reports",
"NO_RECORDS": "There are no CSAT survey responses available.", "NO_RECORDS": "There are no CSAT survey responses available.",
"DOWNLOAD": "Download CSAT Reports",
"FILTERS": { "FILTERS": {
"AGENTS": { "AGENTS": {
"PLACEHOLDER": "Choose Agents" "PLACEHOLDER": "Choose Agents"

View file

@ -3,9 +3,18 @@
<report-filter-selector <report-filter-selector
agents-filter agents-filter
:agents-filter-items-list="agentList" :agents-filter-items-list="agentList"
:show-business-hours-switch="false"
@date-range-change="onDateRangeChange" @date-range-change="onDateRangeChange"
@agents-filter-change="onAgentsFilterChange" @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-metrics />
<csat-table :page-index="pageIndex" @page-change="onPageNumberChange" /> <csat-table :page-index="pageIndex" @page-change="onPageNumberChange" />
</div> </div>
@ -15,6 +24,7 @@ import CsatMetrics from './components/CsatMetrics';
import CsatTable from './components/CsatTable'; import CsatTable from './components/CsatTable';
import ReportFilterSelector from './components/FilterSelector'; import ReportFilterSelector from './components/FilterSelector';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { generateFileName } from '../../../../helper/downloadHelper';
export default { export default {
name: 'CsatResponses', name: 'CsatResponses',
@ -24,7 +34,7 @@ export default {
ReportFilterSelector, ReportFilterSelector,
}, },
data() { data() {
return { pageIndex: 1, from: 0, to: 0, user_ids: [] }; return { pageIndex: 1, from: 0, to: 0, userIds: [] };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
@ -39,7 +49,7 @@ export default {
this.$store.dispatch('csat/getMetrics', { this.$store.dispatch('csat/getMetrics', {
from: this.from, from: this.from,
to: this.to, to: this.to,
user_ids: this.user_ids, user_ids: this.userIds,
}); });
this.getResponses(); this.getResponses();
}, },
@ -48,7 +58,7 @@ export default {
page: this.pageIndex, page: this.pageIndex,
from: this.from, from: this.from,
to: this.to, to: this.to,
user_ids: this.user_ids, user_ids: this.userIds,
}); });
}, },
onPageNumberChange(pageIndex) { onPageNumberChange(pageIndex) {
@ -61,9 +71,18 @@ export default {
this.getAllData(); this.getAllData();
}, },
onAgentsFilterChange(agents) { onAgentsFilterChange(agents) {
this.user_ids = agents.map(el => el.id); this.userIds = agents.map(el => el.id);
this.getAllData(); 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> </script>

View file

@ -61,7 +61,10 @@
@input="handleAgentsFilterSelection" @input="handleAgentsFilterSelection"
/> />
</div> </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"> <span class="business-hours-text margin-right-small">
{{ $t('REPORT.BUSINESS_HOURS') }} {{ $t('REPORT.BUSINESS_HOURS') }}
</span> </span>
@ -105,6 +108,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
showBusinessHoursSwitch: {
type: Boolean,
default: true,
},
}, },
data() { data() {
return { return {

View file

@ -61,6 +61,7 @@ import format from 'date-fns/format';
import { GROUP_BY_FILTER, METRIC_CHART } from '../constants'; import { GROUP_BY_FILTER, METRIC_CHART } from '../constants';
import reportMixin from '../../../../../mixins/reportMixin'; import reportMixin from '../../../../../mixins/reportMixin';
import { formatTime } from '@chatwoot/utils'; import { formatTime } from '@chatwoot/utils';
import { generateFileName } from '../../../../../helper/downloadHelper';
const REPORTS_KEYS = { const REPORTS_KEYS = {
CONVERSATIONS: 'conversations_count', CONVERSATIONS: 'conversations_count',
@ -250,26 +251,17 @@ export default {
}); });
}, },
downloadReports() { downloadReports() {
const { from, to } = this; const { from, to, type } = this;
const fileName = `${this.type}-report-${format( const dispatchMethods = {
fromUnixTime(to), agent: 'downloadAgentReports',
'dd-MM-yyyy' label: 'downloadLabelReports',
)}.csv`; inbox: 'downloadInboxReports',
switch (this.type) { team: 'downloadTeamReports',
case 'agent': };
this.$store.dispatch('downloadAgentReports', { from, to, fileName }); if (dispatchMethods[type]) {
break; const fileName = generateFileName({ type, to });
case 'label': const params = { from, to, fileName };
this.$store.dispatch('downloadLabelReports', { from, to, fileName }); this.$store.dispatch(dispatchMethods[type], params);
break;
case 'inbox':
this.$store.dispatch('downloadInboxReports', { from, to, fileName });
break;
case 'team':
this.$store.dispatch('downloadTeamReports', { from, to, fileName });
break;
default:
break;
} }
}, },
changeSelection(index) { changeSelection(index) {

View file

@ -1,6 +1,7 @@
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import types from '../mutation-types'; import types from '../mutation-types';
import CSATReports from '../../api/csatReports'; import CSATReports from '../../api/csatReports';
import { downloadCsvFile } from '../../helper/downloadHelper';
const computeDistribution = (value, total) => const computeDistribution = (value, total) =>
((value * 100) / total).toFixed(2); ((value * 100) / total).toFixed(2);
@ -107,6 +108,11 @@ export const actions = {
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: false }); 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 = { export const mutations = {

View file

@ -5,7 +5,7 @@ import * as types from '../mutation-types';
import Report from '../../api/reports'; import Report from '../../api/reports';
import Vue from 'vue'; import Vue from 'vue';
import { downloadCsvFile } from '../../helper/downloadCsvFile'; import { downloadCsvFile } from '../../helper/downloadHelper';
const state = { const state = {
fetchingStatus: false, fetchingStatus: false,

View file

@ -6,4 +6,8 @@ class CsatSurveyResponsePolicy < ApplicationPolicy
def metrics? def metrics?
@account_user.administrator? @account_user.administrator?
end end
def download?
@account_user.administrator?
end
end end

View file

@ -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')
)
])
-%>

View file

@ -60,6 +60,16 @@ en:
avg_first_response_time: Avg first response time (Minutes) avg_first_response_time: Avg first response time (Minutes)
avg_resolution_time: Avg resolution time (Minutes) avg_resolution_time: Avg resolution time (Minutes)
default_group_by: day 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: notifications:
notification_title: notification_title:

View file

@ -106,6 +106,7 @@ Rails.application.routes.draw do
resources :csat_survey_responses, only: [:index] do resources :csat_survey_responses, only: [:index] do
collection do collection do
get :metrics get :metrics
get :download
end end
end end
resources :custom_attribute_definitions, only: [:index, :show, :create, :update, :destroy] resources :custom_attribute_definitions, only: [:index, :show, :create, :update, :destroy]

View file

@ -148,4 +148,38 @@ RSpec.describe 'CSAT Survey Responses API', type: :request do
end end
end 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 end