feat: Add Reports for teams (#3116)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
3e99088fe3
commit
1c6a539c0a
15 changed files with 240 additions and 59 deletions
|
@ -68,15 +68,14 @@ class V2::ReportBuilder
|
||||||
.count
|
.count
|
||||||
end
|
end
|
||||||
|
|
||||||
# unscoped removes all scopes added to a model previously
|
|
||||||
def incoming_messages_count
|
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)
|
.group_by_day(:created_at, range: range, default_value: 0)
|
||||||
.count
|
.count
|
||||||
end
|
end
|
||||||
|
|
||||||
def outgoing_messages_count
|
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)
|
.group_by_day(:created_at, range: range, default_value: 0)
|
||||||
.count
|
.count
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,6 +35,12 @@ class ReportsAPI extends ApiClient {
|
||||||
params: { since, until },
|
params: { since, until },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTeamReports(since, until) {
|
||||||
|
return axios.get(`${this.url}/teams`, {
|
||||||
|
params: { since, until },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ReportsAPI();
|
export default new ReportsAPI();
|
||||||
|
|
|
@ -16,6 +16,7 @@ describe('#Reports API', () => {
|
||||||
expect(reportsAPI).toHaveProperty('getAgentReports');
|
expect(reportsAPI).toHaveProperty('getAgentReports');
|
||||||
expect(reportsAPI).toHaveProperty('getLabelReports');
|
expect(reportsAPI).toHaveProperty('getLabelReports');
|
||||||
expect(reportsAPI).toHaveProperty('getInboxReports');
|
expect(reportsAPI).toHaveProperty('getInboxReports');
|
||||||
|
expect(reportsAPI).toHaveProperty('getTeamReports');
|
||||||
});
|
});
|
||||||
describeWithAPIMock('API calls', context => {
|
describeWithAPIMock('API calls', context => {
|
||||||
it('#getAccountReports', () => {
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
6
app/javascript/dashboard/helper/downloadCsvFile.js
Normal file
6
app/javascript/dashboard/helper/downloadCsvFile.js
Normal file
|
@ -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();
|
||||||
|
};
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -250,6 +250,69 @@
|
||||||
"PLACEHOLDER": "Select date range"
|
"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": {
|
"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.",
|
||||||
|
|
|
@ -149,7 +149,8 @@
|
||||||
"ONE_OFF": "One off",
|
"ONE_OFF": "One off",
|
||||||
"REPORTS_AGENT": "Agents",
|
"REPORTS_AGENT": "Agents",
|
||||||
"REPORTS_LABEL": "Labels",
|
"REPORTS_LABEL": "Labels",
|
||||||
"REPORTS_INBOX": "Inbox"
|
"REPORTS_INBOX": "Inbox",
|
||||||
|
"REPORTS_TEAM": "Team"
|
||||||
},
|
},
|
||||||
"CREATE_ACCOUNT": {
|
"CREATE_ACCOUNT": {
|
||||||
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
|
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
|
||||||
|
|
|
@ -7,6 +7,7 @@ const reports = accountId => ({
|
||||||
'agent_reports',
|
'agent_reports',
|
||||||
'label_reports',
|
'label_reports',
|
||||||
'inbox_reports',
|
'inbox_reports',
|
||||||
|
'team_reports',
|
||||||
],
|
],
|
||||||
menuItems: {
|
menuItems: {
|
||||||
back: {
|
back: {
|
||||||
|
@ -31,7 +32,7 @@ const reports = accountId => ({
|
||||||
toStateName: 'csat_reports',
|
toStateName: 'csat_reports',
|
||||||
},
|
},
|
||||||
agentReports: {
|
agentReports: {
|
||||||
icon: 'ion-ios-people',
|
icon: 'ion-person-stalker',
|
||||||
label: 'REPORTS_AGENT',
|
label: 'REPORTS_AGENT',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/reports/agent`),
|
toState: frontendURL(`accounts/${accountId}/reports/agent`),
|
||||||
|
@ -51,6 +52,13 @@ const reports = accountId => ({
|
||||||
toState: frontendURL(`accounts/${accountId}/reports/inboxes`),
|
toState: frontendURL(`accounts/${accountId}/reports/inboxes`),
|
||||||
toStateName: 'inbox_reports',
|
toStateName: 'inbox_reports',
|
||||||
},
|
},
|
||||||
|
teamReports: {
|
||||||
|
icon: 'ion-ios-people',
|
||||||
|
label: 'REPORTS_TEAM',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/reports/teams`),
|
||||||
|
toStateName: 'team_reports',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
<template>
|
||||||
|
<woot-reports
|
||||||
|
key="team-reports"
|
||||||
|
type="team"
|
||||||
|
getter-key="teams/getTeams"
|
||||||
|
action-key="teams/get"
|
||||||
|
:download-button-label="$t('TEAM_REPORTS.DOWNLOAD_TEAM_REPORTS')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import WootReports from './components/WootReports';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
WootReports,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -6,7 +6,7 @@
|
||||||
</p>
|
</p>
|
||||||
<multiselect
|
<multiselect
|
||||||
v-model="currentSelectedFilter"
|
v-model="currentSelectedFilter"
|
||||||
:placeholder="$t('AGENT_REPORTS.FILTER_DROPDOWN_LABEL')"
|
:placeholder="multiselectLabel"
|
||||||
label="name"
|
label="name"
|
||||||
track-by="id"
|
track-by="id"
|
||||||
:options="filterItemsList"
|
:options="filterItemsList"
|
||||||
|
@ -40,13 +40,13 @@
|
||||||
</template>
|
</template>
|
||||||
</multiselect>
|
</multiselect>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="type === 'label'" class="small-12 medium-3 pull-right">
|
<div v-else-if="type === 'label'" class="small-12 medium-3 pull-right">
|
||||||
<p aria-hidden="true" class="hide">
|
<p aria-hidden="true" class="hide">
|
||||||
{{ $t('LABEL_REPORTS.FILTER_DROPDOWN_LABEL') }}
|
{{ $t('LABEL_REPORTS.FILTER_DROPDOWN_LABEL') }}
|
||||||
</p>
|
</p>
|
||||||
<multiselect
|
<multiselect
|
||||||
v-model="currentSelectedFilter"
|
v-model="currentSelectedFilter"
|
||||||
:placeholder="$t('LABEL_REPORTS.FILTER_DROPDOWN_LABEL')"
|
:placeholder="multiselectLabel"
|
||||||
label="title"
|
label="title"
|
||||||
track-by="id"
|
track-by="id"
|
||||||
:options="filterItemsList"
|
:options="filterItemsList"
|
||||||
|
@ -59,11 +59,11 @@
|
||||||
<div
|
<div
|
||||||
:style="{ backgroundColor: props.option.color }"
|
:style="{ backgroundColor: props.option.color }"
|
||||||
class="reports-option__rounded--item margin-right-small"
|
class="reports-option__rounded--item margin-right-small"
|
||||||
></div>
|
/>
|
||||||
<span class="reports-option__desc">
|
<span class="reports-option__desc">
|
||||||
<span class="reports-option__title">{{
|
<span class="reports-option__title">
|
||||||
props.option.title
|
{{ props.option.title }}
|
||||||
}}</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -78,15 +78,15 @@
|
||||||
"
|
"
|
||||||
></div>
|
></div>
|
||||||
<span class="reports-option__desc">
|
<span class="reports-option__desc">
|
||||||
<span class="reports-option__title">{{
|
<span class="reports-option__title">
|
||||||
props.option.title
|
{{ props.option.title }}
|
||||||
}}</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</multiselect>
|
</multiselect>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="type === 'inbox'" class="small-12 medium-3 pull-right">
|
<div v-else class="small-12 medium-3 pull-right">
|
||||||
<p aria-hidden="true" class="hide">
|
<p aria-hidden="true" class="hide">
|
||||||
{{ $t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL') }}
|
{{ $t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL') }}
|
||||||
</p>
|
</p>
|
||||||
|
@ -94,7 +94,7 @@
|
||||||
v-model="currentSelectedFilter"
|
v-model="currentSelectedFilter"
|
||||||
track-by="id"
|
track-by="id"
|
||||||
label="name"
|
label="name"
|
||||||
:placeholder="$t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL')"
|
:placeholder="multiselectLabel"
|
||||||
selected-label
|
selected-label
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
deselect-label=""
|
deselect-label=""
|
||||||
|
@ -185,12 +185,19 @@ export default {
|
||||||
const fromDate = subDays(new Date(), diff);
|
const fromDate = subDays(new Date(), diff);
|
||||||
return this.fromCustomDate(fromDate);
|
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: {
|
watch: {
|
||||||
filterItemsList(val) {
|
filterItemsList(val) {
|
||||||
this.currentSelectedFilter = val[0];
|
this.currentSelectedFilter = val[0];
|
||||||
},
|
|
||||||
currentSelectedFilter() {
|
|
||||||
this.changeFilterSelection();
|
this.changeFilterSelection();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,8 +15,8 @@
|
||||||
@date-range-change="onDateRangeChange"
|
@date-range-change="onDateRangeChange"
|
||||||
@filter-change="onFilterChange"
|
@filter-change="onFilterChange"
|
||||||
/>
|
/>
|
||||||
<div v-if="selectedFilter">
|
<div>
|
||||||
<div class="row">
|
<div v-if="filterItemsList.length" class="row">
|
||||||
<woot-report-stats-card
|
<woot-report-stats-card
|
||||||
v-for="(metric, index) in metrics"
|
v-for="(metric, index) in metrics"
|
||||||
:key="metric.NAME"
|
:key="metric.NAME"
|
||||||
|
@ -34,7 +34,10 @@
|
||||||
:message="$t('REPORT.LOADING_CHART')"
|
:message="$t('REPORT.LOADING_CHART')"
|
||||||
/>
|
/>
|
||||||
<div v-else class="chart-container">
|
<div v-else class="chart-container">
|
||||||
<woot-bar v-if="accountReport.data.length" :collection="collection" />
|
<woot-bar
|
||||||
|
v-if="accountReport.data.length && filterItemsList.length"
|
||||||
|
:collection="collection"
|
||||||
|
/>
|
||||||
<span v-else class="empty-state">
|
<span v-else class="empty-state">
|
||||||
{{ $t('REPORT.NO_ENOUGH_DATA') }}
|
{{ $t('REPORT.NO_ENOUGH_DATA') }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -118,9 +121,15 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
metrics() {
|
metrics() {
|
||||||
const reportKeys = [
|
let reportKeys = ['CONVERSATIONS'];
|
||||||
'CONVERSATIONS',
|
// If report type is agent, we don't need to show
|
||||||
'INCOMING_MESSAGES',
|
// 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',
|
'OUTGOING_MESSAGES',
|
||||||
'FIRST_RESPONSE_TIME',
|
'FIRST_RESPONSE_TIME',
|
||||||
'RESOLUTION_TIME',
|
'RESOLUTION_TIME',
|
||||||
|
@ -175,6 +184,9 @@ export default {
|
||||||
case 'inbox':
|
case 'inbox':
|
||||||
this.$store.dispatch('downloadInboxReports', { from, to, fileName });
|
this.$store.dispatch('downloadInboxReports', { from, to, fileName });
|
||||||
break;
|
break;
|
||||||
|
case 'team':
|
||||||
|
this.$store.dispatch('downloadTeamReports', { from, to, fileName });
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Index from './Index';
|
||||||
import AgentReports from './AgentReports';
|
import AgentReports from './AgentReports';
|
||||||
import LabelReports from './LabelReports';
|
import LabelReports from './LabelReports';
|
||||||
import InboxReports from './InboxReports';
|
import InboxReports from './InboxReports';
|
||||||
|
import TeamReports from './TeamReports';
|
||||||
import CsatResponses from './CsatResponses';
|
import CsatResponses from './CsatResponses';
|
||||||
import SettingsContent from '../Wrapper';
|
import SettingsContent from '../Wrapper';
|
||||||
import { frontendURL } from '../../../../helper/URLHelper';
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,6 +7,8 @@ import fromUnixTime from 'date-fns/fromUnixTime';
|
||||||
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/downloadCsvFile';
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
fetchingStatus: false,
|
fetchingStatus: false,
|
||||||
reportData: [],
|
reportData: [],
|
||||||
|
@ -78,15 +80,7 @@ export const actions = {
|
||||||
downloadAgentReports(_, reportObj) {
|
downloadAgentReports(_, reportObj) {
|
||||||
return Report.getAgentReports(reportObj.from, reportObj.to)
|
return Report.getAgentReports(reportObj.from, reportObj.to)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
let csvContent = 'data:text/csv;charset=utf-8,' + response.data;
|
downloadCsvFile(reportObj.fileName, 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);
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -95,15 +89,7 @@ export const actions = {
|
||||||
downloadLabelReports(_, reportObj) {
|
downloadLabelReports(_, reportObj) {
|
||||||
return Report.getLabelReports(reportObj.from, reportObj.to)
|
return Report.getLabelReports(reportObj.from, reportObj.to)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
let csvContent = 'data:text/csv;charset=utf-8,' + response.data;
|
downloadCsvFile(reportObj.fileName, 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);
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -112,15 +98,16 @@ export const actions = {
|
||||||
downloadInboxReports(_, reportObj) {
|
downloadInboxReports(_, reportObj) {
|
||||||
return Report.getInboxReports(reportObj.from, reportObj.to)
|
return Report.getInboxReports(reportObj.from, reportObj.to)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
let csvContent = 'data:text/csv;charset=utf-8,' + response.data;
|
downloadCsvFile(reportObj.fileName, response.data);
|
||||||
var encodedUri = encodeURI(csvContent);
|
})
|
||||||
var downloadLink = document.createElement('a');
|
.catch(error => {
|
||||||
downloadLink.href = encodedUri;
|
console.error(error);
|
||||||
downloadLink.download = reportObj.fileName;
|
});
|
||||||
|
},
|
||||||
document.body.appendChild(downloadLink);
|
downloadTeamReports(_, reportObj) {
|
||||||
downloadLink.click();
|
return Report.getTeamReports(reportObj.from, reportObj.to)
|
||||||
// document.body.removeChild(downloadLink);
|
.then(response => {
|
||||||
|
downloadCsvFile(reportObj.fileName, response.data);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
|
@ -78,4 +78,25 @@ describe('#actions', () => {
|
||||||
expect(mockInboxDownloadElement.download).toEqual(param.fileName);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -171,14 +171,14 @@ describe ::V2::ReportBuilder do
|
||||||
type: :label,
|
type: :label,
|
||||||
id: label_1.id,
|
id: label_1.id,
|
||||||
since: (Time.zone.today - 3.days).to_time.to_i.to_s,
|
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)
|
builder = V2::ReportBuilder.new(account, params)
|
||||||
metrics = builder.timeseries
|
metrics = builder.timeseries
|
||||||
|
|
||||||
expect(metrics[Time.zone.today]).to be 20
|
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
|
end
|
||||||
|
|
||||||
it 'return outgoing messages count' do
|
it 'return outgoing messages count' do
|
||||||
|
@ -187,14 +187,14 @@ describe ::V2::ReportBuilder do
|
||||||
type: :label,
|
type: :label,
|
||||||
id: label_1.id,
|
id: label_1.id,
|
||||||
since: (Time.zone.today - 3.days).to_time.to_i.to_s,
|
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)
|
builder = V2::ReportBuilder.new(account, params)
|
||||||
metrics = builder.timeseries
|
metrics = builder.timeseries
|
||||||
|
|
||||||
expect(metrics[Time.zone.today]).to be 50
|
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
|
end
|
||||||
|
|
||||||
it 'return resolutions count' do
|
it 'return resolutions count' do
|
||||||
|
@ -203,7 +203,7 @@ describe ::V2::ReportBuilder do
|
||||||
type: :label,
|
type: :label,
|
||||||
id: label_2.id,
|
id: label_2.id,
|
||||||
since: (Time.zone.today - 3.days).to_time.to_i.to_s,
|
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)
|
conversations = account.conversations.where('created_at < ?', 1.day.ago)
|
||||||
|
@ -242,8 +242,8 @@ describe ::V2::ReportBuilder do
|
||||||
metrics = builder.summary
|
metrics = builder.summary
|
||||||
|
|
||||||
expect(metrics[:conversations_count]).to be 5
|
expect(metrics[:conversations_count]).to be 5
|
||||||
expect(metrics[:incoming_messages_count]).to be 25
|
expect(metrics[:incoming_messages_count]).to be 5
|
||||||
expect(metrics[:outgoing_messages_count]).to be 65
|
expect(metrics[:outgoing_messages_count]).to be 15
|
||||||
expect(metrics[:avg_resolution_time]).to be 0
|
expect(metrics[:avg_resolution_time]).to be 0
|
||||||
expect(metrics[:resolutions_count]).to be 0
|
expect(metrics[:resolutions_count]).to be 0
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue