[CW-53] feat: allow downloading heatmap report (#6683)

* feat: add control header slot

* feat: add download API call

* feat: add conversation traffic template

* feat: allow downloading heatmap content

* feat: wire up download

* fix: grid layout for mobile

* chore: revert formatting

* revert: en.yml file

* feat: add conversation traffic text

* feat: disable rule for map block

* test: conversation traffic

* fix: timezone offset

* feat: download report in UTC

* feat: add UTC warning

* chore: revert formatting

* feat: add traffic text

* chore: fix whitespace change
This commit is contained in:
Shivam Mishra
2023-03-20 15:46:29 +05:30
committed by GitHub
parent 4f936aada5
commit e5134c9ef5
10 changed files with 192 additions and 30 deletions

View File

@@ -32,6 +32,11 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
generate_csv('teams_report', 'api/v2/accounts/reports/teams') generate_csv('teams_report', 'api/v2/accounts/reports/teams')
end end
def conversation_traffic
@report_data = generate_conversations_heatmap_report
generate_csv('conversation_traffic_reports', 'api/v2/accounts/reports/conversation_traffic')
end
def conversations def conversations
return head :unprocessable_entity if params[:type].blank? return head :unprocessable_entity if params[:type].blank?

View File

@@ -27,6 +27,27 @@ module Api::V2::Accounts::ReportsHelper
end end
end end
def generate_conversations_heatmap_report
report_params = {
type: :account,
group_by: 'hour',
since: params[:since],
until: params[:until],
metric: 'conversations_count',
business_hours: false
}
data = V2::ReportBuilder.new(Current.account, report_params).build
# data format is { timestamp: 1231242342, value: 3}
# we need to convert it to { date: "2020-01-01", hour: 12, value: 3}
#
# the generated report is **always** in UTC timezone
data.map do |d|
date = Time.zone.at(d[:timestamp]).to_s
[date, d[:value]]
end
end
def generate_report(report_params) def generate_report(report_params)
V2::ReportBuilder.new( V2::ReportBuilder.new(
Current.account, Current.account,

View File

@@ -59,6 +59,12 @@ class ReportsAPI extends ApiClient {
}); });
} }
getConversationTrafficCSV({ from: since, to: until }) {
return axios.get(`${this.url}/conversation_traffic`, {
params: { since, until },
});
}
getLabelReports({ from: since, to: until, businessHours }) { getLabelReports({ from: since, to: until, businessHours }) {
return axios.get(`${this.url}/labels`, { return axios.get(`${this.url}/labels`, {
params: { since, until, business_hours: businessHours }, params: { since, until, business_hours: businessHours },

View File

@@ -40,6 +40,17 @@
<metric-card <metric-card
:header="this.$t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.HEADER')" :header="this.$t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.HEADER')"
> >
<template #control>
<woot-button
icon="arrow-download"
size="small"
variant="smooth"
color-scheme="secondary"
@click="downloadHeatmapData"
>
Download Report
</woot-button>
</template>
<report-heatmap <report-heatmap
:heat-data="accountConversationHeatmap" :heat-data="accountConversationHeatmap"
:is-loading="uiFlags.isFetchingAccountConversationsHeatmap" :is-loading="uiFlags.isFetchingAccountConversationsHeatmap"
@@ -129,6 +140,15 @@ export default {
this.fetchAgentConversationMetric(); this.fetchAgentConversationMetric();
this.fetchHeatmapData(); this.fetchHeatmapData();
}, },
downloadHeatmapData() {
let to = endOfDay(new Date());
let from = startOfDay(subDays(to, 6));
this.$store.dispatch('downloadAccountConversationHeatmap', {
from: getUnixTime(from),
to: getUnixTime(to),
});
},
fetchHeatmapData() { fetchHeatmapData() {
if (this.uiFlags.isFetchingAccountConversationsHeatmap) { if (this.uiFlags.isFetchingAccountConversationsHeatmap) {
return; return;

View File

@@ -1,10 +1,19 @@
<template> <template>
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5>{{ header }}</h5> <slot name="header">
<span class="live"> <div class="card-header--title-area">
<span class="ellipse" /><span>{{ $t('OVERVIEW_REPORTS.LIVE') }}</span> <h5>{{ header }}</h5>
</span> <span class="live">
<span class="ellipse" /><span>{{
$t('OVERVIEW_REPORTS.LIVE')
}}</span>
</span>
</div>
<div class="card-header--control-area">
<slot name="control" />
</div>
</slot>
</div> </div>
<div v-if="!isLoading" class="card-body row"> <div v-if="!isLoading" class="card-body row">
<slot /> <slot />
@@ -42,37 +51,66 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.card { .card {
margin: var(--space-small) !important; margin: var(--space-small) !important;
}
.card-header {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: var(--space-medium);
h5 { .card-header--control-area {
margin-bottom: var(--zero); opacity: 0.2;
transition: opacity 0.2s ease-in-out;
} }
.live { &:hover {
display: flex; .card-header--control-area {
flex-direction: row; opacity: 1;
align-items: center;
padding-right: var(--space-small);
padding-left: var(--space-small);
margin: var(--space-smaller);
background: rgba(37, 211, 102, 0.1);
color: var(--g-400);
font-size: var(--font-size-mini);
.ellipse {
background-color: var(--g-400);
height: var(--space-smaller);
width: var(--space-smaller);
border-radius: var(--border-radius-rounded);
margin-right: var(--space-smaller);
} }
} }
} }
.card-header {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(max-content, 50%));
gap: var(--space-small) 0px;
flex-grow: 1;
width: 100%;
margin-bottom: var(--space-medium);
.card-header--title-area {
display: flex;
flex-direction: row;
align-items: center;
h5 {
margin-bottom: var(--zero);
}
.live {
display: flex;
flex-direction: row;
align-items: center;
padding-right: var(--space-small);
padding-left: var(--space-small);
margin: var(--space-smaller);
background: rgba(37, 211, 102, 0.1);
color: var(--g-400);
font-size: var(--font-size-mini);
.ellipse {
background-color: var(--g-400);
height: var(--space-smaller);
width: var(--space-smaller);
border-radius: var(--border-radius-rounded);
margin-right: var(--space-smaller);
}
}
}
.card-header--control-area {
display: flex;
flex-direction: row;
align-items: center;
justify-content: end;
gap: var(--space-small);
}
}
.card-body { .card-body {
.metric-content { .metric-content {
padding-bottom: var(--space-small); padding-bottom: var(--space-small);

View File

@@ -1,7 +1,7 @@
/* eslint no-console: 0 */ /* eslint no-console: 0 */
import * as types from '../mutation-types'; import * as types from '../mutation-types';
import Report from '../../api/reports'; import Report from '../../api/reports';
import { downloadCsvFile } from '../../helper/downloadHelper'; import { downloadCsvFile, generateFileName } from '../../helper/downloadHelper';
import AnalyticsHelper from '../../helper/AnalyticsHelper'; import AnalyticsHelper from '../../helper/AnalyticsHelper';
import { REPORTS_EVENTS } from '../../helper/AnalyticsHelper/events'; import { REPORTS_EVENTS } from '../../helper/AnalyticsHelper/events';
import { import {
@@ -179,6 +179,26 @@ export const actions = {
console.error(error); console.error(error);
}); });
}, },
downloadAccountConversationHeatmap(_, reportObj) {
Report.getConversationTrafficCSV(reportObj)
.then(response => {
downloadCsvFile(
generateFileName({
type: 'Conversation traffic',
to: reportObj.to,
}),
response.data
);
AnalyticsHelper.track(REPORTS_EVENTS.DOWNLOAD_REPORT, {
reportType: 'conversation_heatmap',
businessHours: false,
});
})
.catch(error => {
console.error(error);
});
},
}; };
const mutations = { const mutations = {

View File

@@ -0,0 +1,12 @@
<% headers = [
I18n.t('reports.conversation_traffic_csv.date'),
I18n.t('reports.conversation_traffic_csv.conversations_count'),
]
%>
<%= CSVSafe.generate_line headers -%>
<% @report_data.each do |row| %>
<%= CSVSafe.generate_line row -%>
<% end %>
<%= CSVSafe.generate_line [I18n.t('reports.period', since: Date.strptime(params[:since], '%s'), until: Date.strptime(params[:until], '%s'))] %>
<%= CSVSafe.generate_line [I18n.t('reports.utc_warning')] %>

View File

@@ -73,6 +73,7 @@ en:
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone
agent_csv: agent_csv:
agent_name: Agent name agent_name: Agent name
conversations_count: Conversations count conversations_count: Conversations count
@@ -94,6 +95,9 @@ en:
conversations_count: Conversations count conversations_count: Conversations count
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)
conversation_traffic_csv:
date: Date and time
conversations_count: No. of conversations
default_group_by: day default_group_by: day
csat: csat:
headers: headers:

View File

@@ -259,6 +259,7 @@ Rails.application.routes.draw do
get :labels get :labels
get :teams get :teams
get :conversations get :conversations
get :conversation_traffic
end end
end end
end end

View File

@@ -363,4 +363,39 @@ RSpec.describe 'Reports API', type: :request do
end end
end end
end end
describe 'GET /api/v2/accounts/:account_id/reports/conversation_traffic' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/conversation_traffic.csv"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
super().merge(
since: 7.days.ago.to_i.to_s,
until: date_timestamp.to_s
)
end
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/conversation_traffic.csv",
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
it 'returns values' do
get "/api/v2/accounts/#{account.id}/reports/conversation_traffic.csv",
params: params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
end
end
end
end end