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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user