feat - Add filter for reports by agent, label and inboxes (#3084)

* Adds filter for agents, labels and inboxes

* Added Inboxes Reports Feature

* Fixed populating of filter dropdown issue

* If applied, fixes code climate style-lint warnings

* Fixes codeclimate warnings

* if applied, Refactors sidebar file to fix codclimate warnings

* if applied, fixes the download reports button for filtered report-data

* If applied, replaces native img tag with thumbnail component

* If applied, replaces hardcoded color string with variable

* If applied, adds a11y labels to multiselect dropdowns

* If applied, Renames reports methods to generic names

* If applied, Adds test cases for Labels and Inboxes

* If applied, write a test spec for fileDownload helper

* if applied, Moves fileDownload method to a utils folder

* If applied, Fixes the report file name type

* Test Spec for Reports Store module

* Fix specs - add restoreAllMocks

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Fayaz Ahmed
2021-09-30 13:13:45 +05:30
committed by GitHub
parent 57abdc4d5f
commit a1563917ba
21 changed files with 1215 additions and 283 deletions

View File

@@ -2,7 +2,7 @@
<div class="row app-wrapper">
<sidebar :route="currentRoute" :class="sidebarClassName"></sidebar>
<section class="app-content columns" :class="contentClassName">
<router-view></router-view>
<router-view :key="$route.path"></router-view>
</section>
</div>
</template>

View File

@@ -0,0 +1,19 @@
<template>
<woot-reports
key="agent-reports"
type="agent"
getter-key="agents/getAgents"
action-key="agents/get"
:download-button-label="$t('REPORT.DOWNLOAD_AGENT_REPORTS')"
/>
</template>
<script>
import WootReports from './components/WootReports';
export default {
components: {
WootReports,
},
};
</script>

View File

@@ -0,0 +1,19 @@
<template>
<woot-reports
key="inbox-reports"
type="inbox"
getter-key="inboxes/getInboxes"
action-key="inboxes/get"
:download-button-label="$t('INBOX_REPORTS.DOWNLOAD_INBOX_REPORTS')"
/>
</template>
<script>
import WootReports from './components/WootReports';
export default {
components: {
WootReports,
},
};
</script>

View File

@@ -0,0 +1,19 @@
<template>
<woot-reports
key="label-reports"
type="label"
getter-key="labels/getLabels"
action-key="labels/get"
:download-button-label="$t('LABEL_REPORTS.DOWNLOAD_LABEL_REPORTS')"
/>
</template>
<script>
import WootReports from './components/WootReports';
export default {
components: {
WootReports,
},
};
</script>

View File

@@ -0,0 +1,224 @@
<template>
<div class="flex-container flex-dir-column medium-flex-dir-row">
<div v-if="type === 'agent'" class="small-12 medium-3 pull-right">
<p aria-hidden="true" class="hide">
{{ $t('AGENT_REPORTS.FILTER_DROPDOWN_LABEL') }}
</p>
<multiselect
v-model="currentSelectedFilter"
:placeholder="$t('AGENT_REPORTS.FILTER_DROPDOWN_LABEL')"
label="name"
track-by="id"
:options="filterItemsList"
:option-height="24"
:show-labels="false"
@input="changeFilterSelection"
>
<template slot="singleLabel" slot-scope="props">
<div class="display-flex">
<thumbnail
src="props.option.thumbnail"
:username="props.option.name"
size="22px"
class="margin-right-small"
/>
<span class="reports-option__desc">
<span class="reports-option__title">{{ props.option.name }}</span>
</span>
</div>
</template>
<template slot="option" slot-scope="props">
<div class="display-flex">
<thumbnail
src="props.option.thumbnail"
:username="props.option.name"
size="22px"
class="margin-right-small"
/>
<p>{{ props.option.name }}</p>
</div>
</template>
</multiselect>
</div>
<div v-if="type === 'label'" class="small-12 medium-3 pull-right">
<p aria-hidden="true" class="hide">
{{ $t('LABEL_REPORTS.FILTER_DROPDOWN_LABEL') }}
</p>
<multiselect
v-model="currentSelectedFilter"
:placeholder="$t('LABEL_REPORTS.FILTER_DROPDOWN_LABEL')"
label="title"
track-by="id"
:options="filterItemsList"
:option-height="24"
:show-labels="false"
@input="changeFilterSelection"
>
<template slot="singleLabel" slot-scope="props">
<div class="display-flex">
<div
:style="{ backgroundColor: props.option.color }"
class="reports-option__rounded--item margin-right-small"
></div>
<span class="reports-option__desc">
<span class="reports-option__title">{{
props.option.title
}}</span>
</span>
</div>
</template>
<template slot="option" slot-scope="props">
<div class="display-flex">
<div
:style="{ backgroundColor: props.option.color }"
class="
reports-option__rounded--item
reports-option__item
reports-option__label--swatch
"
></div>
<span class="reports-option__desc">
<span class="reports-option__title">{{
props.option.title
}}</span>
</span>
</div>
</template>
</multiselect>
</div>
<div v-if="type === 'inbox'" class="small-12 medium-3 pull-right">
<p aria-hidden="true" class="hide">
{{ $t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL') }}
</p>
<multiselect
v-model="currentSelectedFilter"
track-by="id"
label="name"
:placeholder="$t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL')"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:options="filterItemsList"
:searchable="false"
:allow-empty="false"
@input="changeFilterSelection"
/>
</div>
<div class="small-12 medium-3 pull-right margin-left-small">
<multiselect
v-model="currentDateRangeSelection"
track-by="name"
label="name"
:placeholder="$t('FORMS.MULTISELECT.SELECT_ONE')"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:options="dateRange"
:searchable="false"
:allow-empty="false"
@select="changeDateSelection"
/>
</div>
<woot-date-range-picker
v-if="isDateRangeSelected"
show-range
:value="customDateRange"
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
@change="onChange"
/>
</div>
</template>
<script>
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
const CUSTOM_DATE_RANGE_ID = 5;
import subDays from 'date-fns/subDays';
import startOfDay from 'date-fns/startOfDay';
import getUnixTime from 'date-fns/getUnixTime';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
export default {
components: {
WootDateRangePicker,
Thumbnail,
},
props: {
filterItemsList: {
type: Array,
default: () => [],
},
type: {
type: String,
default: 'agent',
},
},
data() {
return {
currentSelectedFilter: null,
currentDateRangeSelection: this.$t('REPORT.DATE_RANGE')[0],
dateRange: this.$t('REPORT.DATE_RANGE'),
customDateRange: [new Date(), new Date()],
};
},
computed: {
isDateRangeSelected() {
return this.currentDateRangeSelection.id === CUSTOM_DATE_RANGE_ID;
},
to() {
if (this.isDateRangeSelected) {
return this.fromCustomDate(this.customDateRange[1]);
}
return this.fromCustomDate(new Date());
},
from() {
if (this.isDateRangeSelected) {
return this.fromCustomDate(this.customDateRange[0]);
}
const dateRange = {
0: 6,
1: 29,
2: 89,
3: 179,
4: 364,
};
const diff = dateRange[this.currentDateRangeSelection.id];
const fromDate = subDays(new Date(), diff);
return this.fromCustomDate(fromDate);
},
},
watch: {
filterItemsList(val) {
this.currentSelectedFilter = val[0];
},
currentSelectedFilter() {
this.changeFilterSelection();
},
},
mounted() {
this.onDateRangeChange();
},
methods: {
onDateRangeChange() {
this.$emit('date-range-change', { from: this.from, to: this.to });
},
fromCustomDate(date) {
return getUnixTime(startOfDay(date));
},
changeDateSelection(selectedRange) {
this.currentDateRangeSelection = selectedRange;
this.onDateRangeChange();
},
changeFilterSelection() {
this.$emit('filter-change', this.currentSelectedFilter);
},
onChange(value) {
this.customDateRange = value;
this.onDateRangeChange();
},
},
};
</script>
<style lang="scss">
@import '~dashboard/assets/scss/widgets/_reports';
</style>

View File

@@ -0,0 +1,199 @@
<template>
<div class="column content-box">
<woot-button
color-scheme="success"
class-names="button--fixed-right-top"
icon="ion-android-download"
@click="downloadReports"
>
{{ downloadButtonLabel }}
</woot-button>
<report-filters
v-if="filterItemsList"
:type="type"
:filter-items-list="filterItemsList"
@date-range-change="onDateRangeChange"
@filter-change="onFilterChange"
/>
<div v-if="selectedFilter">
<div class="row">
<woot-report-stats-card
v-for="(metric, index) in metrics"
:key="metric.NAME"
:desc="metric.DESC"
:heading="metric.NAME"
:index="index"
:on-click="changeSelection"
:point="accountSummary[metric.KEY]"
:selected="index === currentSelection"
/>
</div>
<div class="report-bar">
<woot-loading-state
v-if="accountReport.isFetching"
:message="$t('REPORT.LOADING_CHART')"
/>
<div v-else class="chart-container">
<woot-bar v-if="accountReport.data.length" :collection="collection" />
<span v-else class="empty-state">
{{ $t('REPORT.NO_ENOUGH_DATA') }}
</span>
</div>
</div>
</div>
</div>
</template>
<script>
import ReportFilters from './ReportFilters';
import fromUnixTime from 'date-fns/fromUnixTime';
import format from 'date-fns/format';
const REPORTS_KEYS = {
CONVERSATIONS: 'conversations_count',
INCOMING_MESSAGES: 'incoming_messages_count',
OUTGOING_MESSAGES: 'outgoing_messages_count',
FIRST_RESPONSE_TIME: 'avg_first_response_time',
RESOLUTION_TIME: 'avg_resolution_time',
RESOLUTION_COUNT: 'resolutions_count',
};
export default {
components: {
ReportFilters,
},
props: {
type: {
type: String,
default: 'account',
},
getterKey: {
type: String,
default: '',
},
actionKey: {
type: String,
default: '',
},
downloadButtonLabel: {
type: String,
default: 'Download Reports',
},
},
data() {
return {
from: 0,
to: 0,
currentSelection: 0,
selectedFilter: null,
};
},
computed: {
filterItemsList() {
return this.$store.getters[this.getterKey] || [];
},
accountSummary() {
return this.$store.getters.getAccountSummary || [];
},
accountReport() {
return this.$store.getters.getAccountReports || [];
},
collection() {
if (this.accountReport.isFetching) {
return {};
}
if (!this.accountReport.data.length) return {};
const labels = this.accountReport.data.map(element =>
format(fromUnixTime(element.timestamp), 'dd/MMM')
);
const data = this.accountReport.data.map(element => element.value);
return {
labels,
datasets: [
{
label: this.metrics[this.currentSelection].NAME,
backgroundColor: '#1f93ff',
data,
},
],
};
},
metrics() {
const reportKeys = [
'CONVERSATIONS',
'INCOMING_MESSAGES',
'OUTGOING_MESSAGES',
'FIRST_RESPONSE_TIME',
'RESOLUTION_TIME',
'RESOLUTION_COUNT',
];
return reportKeys.map(key => ({
NAME: this.$t(`REPORT.METRICS.${key}.NAME`),
KEY: REPORTS_KEYS[key],
DESC: this.$t(`REPORT.METRICS.${key}.DESC`),
}));
},
},
mounted() {
this.$store.dispatch(this.actionKey);
},
methods: {
fetchAllData() {
if (this.selectedFilter) {
const { from, to } = this;
this.$store.dispatch('fetchAccountSummary', {
from,
to,
type: this.type,
id: this.selectedFilter.id,
});
this.fetchChartData();
}
},
fetchChartData() {
const { from, to } = this;
this.$store.dispatch('fetchAccountReport', {
metric: this.metrics[this.currentSelection].KEY,
from,
to,
type: this.type,
id: this.selectedFilter.id,
});
},
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;
default:
break;
}
},
changeSelection(index) {
this.currentSelection = index;
this.fetchChartData();
},
onDateRangeChange({ from, to }) {
this.from = from;
this.to = to;
this.fetchAllData();
},
onFilterChange(payload) {
if (payload) {
this.selectedFilter = payload;
this.fetchAllData();
}
},
},
};
</script>

View File

@@ -1,4 +1,7 @@
import Index from './Index';
import AgentReports from './AgentReports';
import LabelReports from './LabelReports';
import InboxReports from './InboxReports';
import CsatResponses from './CsatResponses';
import SettingsContent from '../Wrapper';
import { frontendURL } from '../../../../helper/URLHelper';
@@ -41,5 +44,53 @@ export default {
},
],
},
{
path: frontendURL('accounts/:accountId/reports'),
component: SettingsContent,
props: {
headerTitle: 'AGENT_REPORTS.HEADER',
icon: 'ion-people',
},
children: [
{
path: 'agent',
name: 'agent_reports',
roles: ['administrator'],
component: AgentReports,
},
],
},
{
path: frontendURL('accounts/:accountId/reports'),
component: SettingsContent,
props: {
headerTitle: 'LABEL_REPORTS.HEADER',
icon: 'ion-pricetags',
},
children: [
{
path: 'label',
name: 'label_reports',
roles: ['administrator'],
component: LabelReports,
},
],
},
{
path: frontendURL('accounts/:accountId/reports'),
component: SettingsContent,
props: {
headerTitle: 'INBOX_REPORTS.HEADER',
icon: 'ion-archive',
},
children: [
{
path: 'inboxes',
name: 'inbox_reports',
roles: ['administrator'],
component: InboxReports,
},
],
},
],
};