mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 03:27:52 +00:00
feat: more CSAT filters (#7038)
* refactor: use grid instead of flex * refactor: let the parent layout decide the spacing * feat: add a separate date-range component * refactor: use new date-range component * fix: destructure all options * refactor: separate group by component * refactor: better handle group by data * fix: defaul group by * refactor: variable naming * refactor: use DATE_RANGE_OPTIONS directly * chore: update platform in gemfile.lock * refactor: trigger fetch on filter change * refactor: remove redundant method * refactor: simplify methods and emitting * refactor: simplify filter logic * refactor: simplify fetching * refactor: imports * refactor: prop name * refactor: CSAT response to use new APIs * refactor: use common filter event * refactor: use computed value for validGroupBy * refactor: better function names * refactor: rename prop * refactor: remove redundant props * refactor: separate agents filter component * feat: add labels filter * feat: add inboxes filter * fix: event * refactor: send label and inbox along with request payload * feat: add inbox filter * feat: add inbox to download * refactor: use request payload from computed property * refactor: params * feat: add team to csat filters * feat: add team to csat filters * feat: add filter for rating * feat: reverse options * feat: add labels for ratings and translations * feat: update translation * fix: margin and spacing * fix: trailing whitespace * feat: add tests for filters * chore: move files * feat: add try catch with alerts * feat: update import * fix: imports * Updates broken imports --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
This commit is contained in:
@@ -784,6 +784,7 @@ GEM
|
|||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-20
|
arm64-darwin-20
|
||||||
|
arm64-darwin-22
|
||||||
arm64-darwin-21
|
arm64-darwin-21
|
||||||
ruby
|
ruby
|
||||||
x86_64-darwin-18
|
x86_64-darwin-18
|
||||||
|
|||||||
@@ -34,9 +34,12 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_csat_survey_responses
|
def set_csat_survey_responses
|
||||||
@csat_survey_responses = filtrate(
|
base_query = Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
||||||
Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
@csat_survey_responses = filtrate(base_query).filter_by_created_at(range)
|
||||||
).filter_by_created_at(range).filter_by_assigned_agent_id(params[:user_ids])
|
.filter_by_assigned_agent_id(params[:user_ids])
|
||||||
|
.filter_by_inbox_id(params[:inbox_id])
|
||||||
|
.filter_by_team_id(params[:team_id])
|
||||||
|
.filter_by_rating(params[:rating])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_current_page_surveys
|
def set_current_page_surveys
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class CSATReportsAPI extends ApiClient {
|
|||||||
super('csat_survey_responses', { accountScoped: true });
|
super('csat_survey_responses', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
get({ page, from, to, user_ids } = {}) {
|
get({ page, from, to, user_ids, inbox_id, team_id, rating } = {}) {
|
||||||
return axios.get(this.url, {
|
return axios.get(this.url, {
|
||||||
params: {
|
params: {
|
||||||
page,
|
page,
|
||||||
@@ -14,24 +14,31 @@ class CSATReportsAPI extends ApiClient {
|
|||||||
until: to,
|
until: to,
|
||||||
sort: '-created_at',
|
sort: '-created_at',
|
||||||
user_ids,
|
user_ids,
|
||||||
|
inbox_id,
|
||||||
|
team_id,
|
||||||
|
rating,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
download({ from, to, user_ids } = {}) {
|
download({ from, to, user_ids, inbox_id, team_id, rating } = {}) {
|
||||||
return axios.get(`${this.url}/download`, {
|
return axios.get(`${this.url}/download`, {
|
||||||
params: {
|
params: {
|
||||||
since: from,
|
since: from,
|
||||||
until: to,
|
until: to,
|
||||||
sort: '-created_at',
|
sort: '-created_at',
|
||||||
user_ids,
|
user_ids,
|
||||||
|
inbox_id,
|
||||||
|
team_id,
|
||||||
|
rating,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getMetrics({ from, to, user_ids } = {}) {
|
getMetrics({ from, to, user_ids, inbox_id, team_id } = {}) {
|
||||||
|
// no ratings for metrics
|
||||||
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, inbox_id, team_id },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.date-picker {
|
.date-picker {
|
||||||
.mx-datepicker {
|
&.no-margin {
|
||||||
width: 100%;
|
.mx-input {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx-datepicker-range {
|
&:not(.auto-width) {
|
||||||
width: 320px;
|
.mx-datepicker-range {
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx-datepicker {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx-input {
|
.mx-input {
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.multiselect {
|
.multiselect {
|
||||||
margin-bottom: var(--space-normal);
|
&:not(.no-margin) {
|
||||||
|
margin-bottom: var(--space-normal);
|
||||||
|
}
|
||||||
|
|
||||||
&.multiselect--disabled {
|
&.multiselect--disabled {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
{
|
{
|
||||||
"CSAT": {
|
"CSAT": {
|
||||||
"TITLE": "Rate your conversation",
|
"TITLE": "Rate your conversation",
|
||||||
"PLACEHOLDER": "Tell us more..."
|
"PLACEHOLDER": "Tell us more...",
|
||||||
|
"RATINGS": {
|
||||||
|
"POOR": "😞 Poor",
|
||||||
|
"FAIR": "😑 Fair",
|
||||||
|
"AVERAGE": "😐 Average",
|
||||||
|
"GOOD": "😀 Good",
|
||||||
|
"EXCELLENT": "😍 Excellent"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
"LOADING_CHART": "Loading chart data...",
|
"LOADING_CHART": "Loading chart data...",
|
||||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||||
"DOWNLOAD_AGENT_REPORTS": "Download agent reports",
|
"DOWNLOAD_AGENT_REPORTS": "Download agent reports",
|
||||||
|
"DATA_FETCHING_FAILED": "Failed to fetch data, please try again later.",
|
||||||
|
"SUMMARY_FETCHING_FAILED": "Failed to fetch summary, please try again later.",
|
||||||
"METRICS": {
|
"METRICS": {
|
||||||
"CONVERSATIONS": {
|
"CONVERSATIONS": {
|
||||||
"NAME": "Conversations",
|
"NAME": "Conversations",
|
||||||
@@ -34,6 +36,14 @@
|
|||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"DATE_RANGE_OPTIONS": {
|
||||||
|
"LAST_7_DAYS": "Last 7 days",
|
||||||
|
"LAST_30_DAYS": "Last 30 days",
|
||||||
|
"LAST_3_MONTHS": "Last 3 months",
|
||||||
|
"LAST_6_MONTHS": "Last 6 months",
|
||||||
|
"LAST_YEAR": "Last year",
|
||||||
|
"CUSTOM_DATE_RANGE": "Custom date range"
|
||||||
|
},
|
||||||
"DATE_RANGE": [
|
"DATE_RANGE": [
|
||||||
{
|
{
|
||||||
"id": 0,
|
"id": 0,
|
||||||
@@ -66,6 +76,12 @@
|
|||||||
},
|
},
|
||||||
"GROUP_BY_FILTER_DROPDOWN_LABEL": "Group By",
|
"GROUP_BY_FILTER_DROPDOWN_LABEL": "Group By",
|
||||||
"DURATION_FILTER_LABEL": "Duration",
|
"DURATION_FILTER_LABEL": "Duration",
|
||||||
|
"GROUPING_OPTIONS": {
|
||||||
|
"DAY": "Day",
|
||||||
|
"WEEK": "Week",
|
||||||
|
"MONTH": "Month",
|
||||||
|
"YEAR": "Year"
|
||||||
|
},
|
||||||
"GROUP_BY_DAY_OPTIONS": [{ "id": 1, "groupBy": "Day" }],
|
"GROUP_BY_DAY_OPTIONS": [{ "id": 1, "groupBy": "Day" }],
|
||||||
"GROUP_BY_WEEK_OPTIONS": [
|
"GROUP_BY_WEEK_OPTIONS": [
|
||||||
{ "id": 1, "groupBy": "Day" },
|
{ "id": 1, "groupBy": "Day" },
|
||||||
@@ -356,6 +372,7 @@
|
|||||||
"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",
|
"DOWNLOAD": "Download CSAT Reports",
|
||||||
|
"DOWNLOAD_FAILED": "Failed to download CSAT Reports",
|
||||||
"FILTERS": {
|
"FILTERS": {
|
||||||
"AGENTS": {
|
"AGENTS": {
|
||||||
"PLACEHOLDER": "Choose Agents"
|
"PLACEHOLDER": "Choose Agents"
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="column content-box">
|
<div class="column content-box">
|
||||||
<report-filter-selector
|
<report-filter-selector
|
||||||
agents-filter
|
:show-agents-filter="true"
|
||||||
:agents-filter-items-list="agentList"
|
:show-inbox-filter="true"
|
||||||
|
:show-rating-filter="true"
|
||||||
|
:show-team-filter="isTeamsEnabled"
|
||||||
:show-business-hours-switch="false"
|
:show-business-hours-switch="false"
|
||||||
@date-range-change="onDateRangeChange"
|
@filter-change="onFilterChange"
|
||||||
@agents-filter-change="onAgentsFilterChange"
|
|
||||||
/>
|
/>
|
||||||
<woot-button
|
<woot-button
|
||||||
color-scheme="success"
|
color-scheme="success"
|
||||||
@@ -23,9 +24,11 @@
|
|||||||
import CsatMetrics from './components/CsatMetrics';
|
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 { generateFileName } from '../../../../helper/downloadHelper';
|
import { generateFileName } from '../../../../helper/downloadHelper';
|
||||||
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||||
|
import alertMixin from '../../../../../shared/mixins/alertMixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CsatResponses',
|
name: 'CsatResponses',
|
||||||
@@ -34,39 +37,78 @@ export default {
|
|||||||
CsatTable,
|
CsatTable,
|
||||||
ReportFilterSelector,
|
ReportFilterSelector,
|
||||||
},
|
},
|
||||||
|
mixins: [alertMixin],
|
||||||
data() {
|
data() {
|
||||||
return { pageIndex: 1, from: 0, to: 0, userIds: [] };
|
return {
|
||||||
|
pageIndex: 1,
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
userIds: [],
|
||||||
|
inbox: null,
|
||||||
|
team: null,
|
||||||
|
rating: null,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
agentList: 'agents/getAgents',
|
accountId: 'getCurrentAccountId',
|
||||||
|
isFeatureEnabledOnAccount: 'accounts/isFeatureEnabledonAccount',
|
||||||
}),
|
}),
|
||||||
},
|
requestPayload() {
|
||||||
mounted() {
|
return {
|
||||||
this.$store.dispatch('agents/get');
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getAllData() {
|
|
||||||
this.$store.dispatch('csat/getMetrics', {
|
|
||||||
from: this.from,
|
from: this.from,
|
||||||
to: this.to,
|
to: this.to,
|
||||||
user_ids: this.userIds,
|
user_ids: this.userIds,
|
||||||
});
|
inbox_id: this.inbox,
|
||||||
this.getResponses();
|
team_id: this.team,
|
||||||
|
rating: this.rating,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
isTeamsEnabled() {
|
||||||
|
return this.isFeatureEnabledOnAccount(
|
||||||
|
this.accountId,
|
||||||
|
FEATURE_FLAGS.TEAM_MANAGEMENT
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getAllData() {
|
||||||
|
try {
|
||||||
|
this.$store.dispatch('csat/getMetrics', this.requestPayload);
|
||||||
|
this.getResponses();
|
||||||
|
} catch {
|
||||||
|
this.showAlert(this.$t('REPORT.DATA_FETCHING_FAILED'));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getResponses() {
|
getResponses() {
|
||||||
this.$store.dispatch('csat/get', {
|
this.$store.dispatch('csat/get', {
|
||||||
page: this.pageIndex,
|
page: this.pageIndex,
|
||||||
from: this.from,
|
...this.requestPayload,
|
||||||
to: this.to,
|
|
||||||
user_ids: this.userIds,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
downloadReports() {
|
||||||
|
const type = 'csat';
|
||||||
|
try {
|
||||||
|
this.$store.dispatch('csat/downloadCSATReports', {
|
||||||
|
fileName: generateFileName({ type, to: this.to }),
|
||||||
|
...this.requestPayload,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.showAlert(this.$t('REPORT.CSAT_REPORTS.DOWNLOAD_FAILED'));
|
||||||
|
}
|
||||||
|
},
|
||||||
onPageNumberChange(pageIndex) {
|
onPageNumberChange(pageIndex) {
|
||||||
this.pageIndex = pageIndex;
|
this.pageIndex = pageIndex;
|
||||||
this.getResponses();
|
this.getResponses();
|
||||||
},
|
},
|
||||||
onDateRangeChange({ from, to }) {
|
onFilterChange({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
selectedAgents,
|
||||||
|
selectedInbox,
|
||||||
|
selectedTeam,
|
||||||
|
selectedRating,
|
||||||
|
}) {
|
||||||
// do not track filter change on inital load
|
// do not track filter change on inital load
|
||||||
if (this.from !== 0 && this.to !== 0) {
|
if (this.from !== 0 && this.to !== 0) {
|
||||||
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
|
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
|
||||||
@@ -74,27 +116,16 @@ export default {
|
|||||||
reportType: 'csat',
|
reportType: 'csat',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.from = from;
|
this.from = from;
|
||||||
this.to = to;
|
this.to = to;
|
||||||
|
this.userIds = selectedAgents.map(el => el.id);
|
||||||
|
this.inbox = selectedInbox?.id;
|
||||||
|
this.team = selectedTeam?.id;
|
||||||
|
this.rating = selectedRating?.value;
|
||||||
|
|
||||||
this.getAllData();
|
this.getAllData();
|
||||||
},
|
},
|
||||||
onAgentsFilterChange(agents) {
|
|
||||||
this.userIds = agents.map(el => el.id);
|
|
||||||
this.getAllData();
|
|
||||||
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
|
|
||||||
filterType: 'agent',
|
|
||||||
reportType: 'csat',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
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>
|
||||||
|
|||||||
@@ -9,12 +9,9 @@
|
|||||||
{{ $t('REPORT.DOWNLOAD_AGENT_REPORTS') }}
|
{{ $t('REPORT.DOWNLOAD_AGENT_REPORTS') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<report-filter-selector
|
<report-filter-selector
|
||||||
group-by-filter
|
:show-agents-filter="false"
|
||||||
:selected-group-by-filter="selectedGroupByFilter"
|
:show-group-by-filter="true"
|
||||||
:filter-items-list="filterItemsList"
|
|
||||||
@date-range-change="onDateRangeChange"
|
|
||||||
@filter-change="onFilterChange"
|
@filter-change="onFilterChange"
|
||||||
@business-hours-toggle="onBusinessHoursToggle"
|
|
||||||
/>
|
/>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<woot-report-stats-card
|
<woot-report-stats-card
|
||||||
@@ -55,7 +52,8 @@ import fromUnixTime from 'date-fns/fromUnixTime';
|
|||||||
import format from 'date-fns/format';
|
import format from 'date-fns/format';
|
||||||
import ReportFilterSelector from './components/FilterSelector';
|
import ReportFilterSelector from './components/FilterSelector';
|
||||||
import { GROUP_BY_FILTER, METRIC_CHART } from './constants';
|
import { GROUP_BY_FILTER, METRIC_CHART } from './constants';
|
||||||
import reportMixin from '../../../../mixins/reportMixin';
|
import reportMixin from 'dashboard/mixins/reportMixin';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import { formatTime } from '@chatwoot/utils';
|
import { formatTime } from '@chatwoot/utils';
|
||||||
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||||
|
|
||||||
@@ -73,15 +71,13 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
ReportFilterSelector,
|
ReportFilterSelector,
|
||||||
},
|
},
|
||||||
mixins: [reportMixin],
|
mixins: [reportMixin, alertMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
from: 0,
|
from: 0,
|
||||||
to: 0,
|
to: 0,
|
||||||
currentSelection: 0,
|
currentSelection: 0,
|
||||||
groupBy: GROUP_BY_FILTER[1],
|
groupBy: GROUP_BY_FILTER[1],
|
||||||
filterItemsList: this.$t('REPORT.GROUP_BY_DAY_OPTIONS'),
|
|
||||||
selectedGroupByFilter: {},
|
|
||||||
businessHours: false,
|
businessHours: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -191,24 +187,35 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchAllData() {
|
fetchAllData() {
|
||||||
const { from, to, groupBy, businessHours } = this;
|
this.fetchAccountSummary();
|
||||||
this.$store.dispatch('fetchAccountSummary', {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
groupBy: groupBy.period,
|
|
||||||
businessHours,
|
|
||||||
});
|
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
},
|
},
|
||||||
|
fetchAccountSummary() {
|
||||||
|
try {
|
||||||
|
this.$store.dispatch('fetchAccountSummary', this.getRequestPayload());
|
||||||
|
} catch {
|
||||||
|
this.showAlert(this.$t('REPORT.SUMMARY_FETCHING_FAILED'));
|
||||||
|
}
|
||||||
|
},
|
||||||
fetchChartData() {
|
fetchChartData() {
|
||||||
|
try {
|
||||||
|
this.$store.dispatch('fetchAccountReport', {
|
||||||
|
metric: this.metrics[this.currentSelection].KEY,
|
||||||
|
...this.getRequestPayload(),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
this.showAlert(this.$t('REPORT.DATA_FETCHING_FAILED'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getRequestPayload() {
|
||||||
const { from, to, groupBy, businessHours } = this;
|
const { from, to, groupBy, businessHours } = this;
|
||||||
this.$store.dispatch('fetchAccountReport', {
|
|
||||||
metric: this.metrics[this.currentSelection].KEY,
|
return {
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
groupBy: groupBy.period,
|
groupBy: groupBy.period,
|
||||||
businessHours,
|
businessHours,
|
||||||
});
|
};
|
||||||
},
|
},
|
||||||
downloadAgentReports() {
|
downloadAgentReports() {
|
||||||
const { from, to } = this;
|
const { from, to } = this;
|
||||||
@@ -222,57 +229,15 @@ export default {
|
|||||||
this.currentSelection = index;
|
this.currentSelection = index;
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
},
|
},
|
||||||
onDateRangeChange({ from, to, groupBy }) {
|
onFilterChange({ from, to, groupBy, businessHours }) {
|
||||||
// do not track filter change on inital load
|
|
||||||
if (this.from !== 0 && this.to !== 0) {
|
|
||||||
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
|
|
||||||
filterType: 'date',
|
|
||||||
reportType: 'conversations',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.from = from;
|
this.from = from;
|
||||||
this.to = to;
|
this.to = to;
|
||||||
this.filterItemsList = this.fetchFilterItems(groupBy);
|
this.groupBy = groupBy;
|
||||||
const filterItems = this.filterItemsList.filter(
|
this.businessHours = businessHours;
|
||||||
item => item.id === this.groupBy.id
|
|
||||||
);
|
|
||||||
if (filterItems.length > 0) {
|
|
||||||
this.selectedGroupByFilter = filterItems[0];
|
|
||||||
} else {
|
|
||||||
this.selectedGroupByFilter = this.filterItemsList[0];
|
|
||||||
this.groupBy = GROUP_BY_FILTER[this.selectedGroupByFilter.id];
|
|
||||||
}
|
|
||||||
this.fetchAllData();
|
|
||||||
},
|
|
||||||
onFilterChange(payload) {
|
|
||||||
this.groupBy = GROUP_BY_FILTER[payload.id];
|
|
||||||
this.fetchAllData();
|
this.fetchAllData();
|
||||||
|
|
||||||
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
|
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
|
||||||
filterType: 'groupBy',
|
filterValue: { from, to, groupBy, businessHours },
|
||||||
filterValue: this.groupBy?.period,
|
|
||||||
reportType: 'conversations',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
fetchFilterItems(groupBy) {
|
|
||||||
switch (groupBy) {
|
|
||||||
case GROUP_BY_FILTER[2].period:
|
|
||||||
return this.$t('REPORT.GROUP_BY_WEEK_OPTIONS');
|
|
||||||
case GROUP_BY_FILTER[3].period:
|
|
||||||
return this.$t('REPORT.GROUP_BY_MONTH_OPTIONS');
|
|
||||||
case GROUP_BY_FILTER[4].period:
|
|
||||||
return this.$t('REPORT.GROUP_BY_YEAR_OPTIONS');
|
|
||||||
default:
|
|
||||||
return this.$t('REPORT.GROUP_BY_DAY_OPTIONS');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onBusinessHoursToggle(value) {
|
|
||||||
this.businessHours = value;
|
|
||||||
this.fetchAllData();
|
|
||||||
|
|
||||||
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
|
|
||||||
filterType: 'businessHours',
|
|
||||||
filterValue: value,
|
|
||||||
reportType: 'conversations',
|
reportType: 'conversations',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,72 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex-container flex-dir-column medium-flex-dir-row">
|
<div class="filter-container">
|
||||||
<div class="small-12 medium-3 pull-right multiselect-wrap--small">
|
<reports-filters-date-range @on-range-change="onDateRangeChange" />
|
||||||
<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
|
<woot-date-range-picker
|
||||||
v-if="isDateRangeSelected"
|
v-if="isDateRangeSelected"
|
||||||
class="margin-left-1"
|
|
||||||
show-range
|
show-range
|
||||||
|
class="no-margin auto-width"
|
||||||
:value="customDateRange"
|
:value="customDateRange"
|
||||||
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
|
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
|
||||||
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
|
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
|
||||||
@change="onChange"
|
@change="onCustomDateRangeChange"
|
||||||
/>
|
/>
|
||||||
<div
|
<reports-filters-date-group-by
|
||||||
v-if="notLast7Days && groupByFilter"
|
v-if="showGroupByFilter && isGroupByPossible"
|
||||||
class="small-12 medium-3 pull-right margin-left-1 margin-right-1 multiselect-wrap--small"
|
:valid-group-options="validGroupOptions"
|
||||||
>
|
:selected-option="selectedGroupByFilter"
|
||||||
<p aria-hidden="true" class="hide">
|
@on-grouping-change="onGroupingChange"
|
||||||
{{ $t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL') }}
|
/>
|
||||||
</p>
|
<reports-filters-agents
|
||||||
<multiselect
|
v-if="showAgentsFilter"
|
||||||
v-model="currentSelectedFilter"
|
@agents-filter-selection="handleAgentsFilterSelection"
|
||||||
track-by="id"
|
/>
|
||||||
label="groupBy"
|
<reports-filters-labels
|
||||||
:placeholder="$t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL')"
|
v-if="showLabelsFilter"
|
||||||
:options="filterItemsList"
|
@labels-filter-selection="handleLabelsFilterSelection"
|
||||||
:allow-empty="false"
|
/>
|
||||||
:show-labels="false"
|
<reports-filters-teams
|
||||||
@input="changeFilterSelection"
|
v-if="showTeamFilter"
|
||||||
/>
|
@team-filter-selection="handleTeamFilterSelection"
|
||||||
</div>
|
/>
|
||||||
<div
|
<reports-filters-inboxes
|
||||||
v-if="agentsFilter"
|
v-if="showInboxFilter"
|
||||||
class="small-12 medium-3 pull-right margin-left-1 margin-right-1 multiselect-wrap--small"
|
@inbox-filter-selection="handleInboxFilterSelection"
|
||||||
>
|
/>
|
||||||
<multiselect
|
<reports-filters-ratings
|
||||||
v-model="selectedAgents"
|
v-if="showRatingFilter"
|
||||||
:options="agentsFilterItemsList"
|
@rating-filter-selection="handleRatingFilterSelection"
|
||||||
track-by="id"
|
/>
|
||||||
label="name"
|
<div v-if="showBusinessHoursSwitch" class="business-hours">
|
||||||
:multiple="true"
|
<span class="business-hours-text ">
|
||||||
:close-on-select="false"
|
|
||||||
:clear-on-select="false"
|
|
||||||
:hide-selected="true"
|
|
||||||
:placeholder="$t('CSAT_REPORTS.FILTERS.AGENTS.PLACEHOLDER')"
|
|
||||||
selected-label
|
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
|
||||||
:deselect-label="$t('FORMS.MULTISELECT.ENTER_TO_REMOVE')"
|
|
||||||
@input="handleAgentsFilterSelection"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="showBusinessHoursSwitch"
|
|
||||||
class="small-12 medium-3 business-hours"
|
|
||||||
>
|
|
||||||
<span class="business-hours-text margin-right-1">
|
|
||||||
{{ $t('REPORT.BUSINESS_HOURS') }}
|
{{ $t('REPORT.BUSINESS_HOURS') }}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
@@ -77,36 +48,54 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
||||||
|
import ReportsFiltersDateRange from './Filters/DateRange.vue';
|
||||||
|
import ReportsFiltersDateGroupBy from './Filters/DateGroupBy.vue';
|
||||||
|
import ReportsFiltersAgents from './Filters/Agents.vue';
|
||||||
|
import ReportsFiltersLabels from './Filters/Labels.vue';
|
||||||
|
import ReportsFiltersInboxes from './Filters/Inboxes.vue';
|
||||||
|
import ReportsFiltersTeams from './Filters/Teams.vue';
|
||||||
|
import ReportsFiltersRatings from './Filters/Ratings.vue';
|
||||||
import subDays from 'date-fns/subDays';
|
import subDays from 'date-fns/subDays';
|
||||||
import startOfDay from 'date-fns/startOfDay';
|
import { DATE_RANGE_OPTIONS } from '../constants';
|
||||||
import getUnixTime from 'date-fns/getUnixTime';
|
import { getUnixStartOfDay, getUnixEndOfDay } from 'helpers/DateHelper';
|
||||||
import { GROUP_BY_FILTER } from '../constants';
|
|
||||||
import endOfDay from 'date-fns/endOfDay';
|
|
||||||
|
|
||||||
const CUSTOM_DATE_RANGE_ID = 5;
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
WootDateRangePicker,
|
WootDateRangePicker,
|
||||||
|
ReportsFiltersDateRange,
|
||||||
|
ReportsFiltersDateGroupBy,
|
||||||
|
ReportsFiltersAgents,
|
||||||
|
ReportsFiltersLabels,
|
||||||
|
ReportsFiltersInboxes,
|
||||||
|
ReportsFiltersTeams,
|
||||||
|
ReportsFiltersRatings,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
filterItemsList: {
|
filterItemsList: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
agentsFilterItemsList: {
|
showGroupByFilter: {
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
selectedGroupByFilter: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
groupByFilter: {
|
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
agentsFilter: {
|
showAgentsFilter: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showLabelsFilter: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showInboxFilter: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showRatingFilter: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showTeamFilter: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
@@ -117,95 +106,134 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
currentDateRangeSelection: this.$t('REPORT.DATE_RANGE')[0],
|
// default value, need not be translated
|
||||||
dateRange: this.$t('REPORT.DATE_RANGE'),
|
selectedDateRange: DATE_RANGE_OPTIONS.LAST_7_DAYS,
|
||||||
customDateRange: [new Date(), new Date()],
|
selectedGroupByFilter: null,
|
||||||
currentSelectedFilter: null,
|
selectedLabel: null,
|
||||||
|
selectedInbox: null,
|
||||||
|
selectedTeam: null,
|
||||||
|
selectedRating: null,
|
||||||
selectedAgents: [],
|
selectedAgents: [],
|
||||||
|
customDateRange: [new Date(), new Date()],
|
||||||
businessHoursSelected: false,
|
businessHoursSelected: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isDateRangeSelected() {
|
isDateRangeSelected() {
|
||||||
return this.currentDateRangeSelection.id === CUSTOM_DATE_RANGE_ID;
|
return (
|
||||||
|
this.selectedDateRange.id === DATE_RANGE_OPTIONS.CUSTOM_DATE_RANGE.id
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isGroupByPossible() {
|
||||||
|
return this.selectedDateRange.id !== DATE_RANGE_OPTIONS.LAST_7_DAYS.id;
|
||||||
},
|
},
|
||||||
to() {
|
to() {
|
||||||
if (this.isDateRangeSelected) {
|
if (this.isDateRangeSelected) {
|
||||||
return this.toCustomDate(this.customDateRange[1]);
|
return getUnixEndOfDay(this.customDateRange[1]);
|
||||||
}
|
}
|
||||||
return this.toCustomDate(new Date());
|
return getUnixEndOfDay(new Date());
|
||||||
},
|
},
|
||||||
from() {
|
from() {
|
||||||
if (this.isDateRangeSelected) {
|
if (this.isDateRangeSelected) {
|
||||||
return this.fromCustomDate(this.customDateRange[0]);
|
return getUnixStartOfDay(this.customDateRange[0]);
|
||||||
}
|
}
|
||||||
const dateRange = {
|
|
||||||
0: 6,
|
const { offset } = this.selectedDateRange;
|
||||||
1: 29,
|
const fromDate = subDays(new Date(), offset);
|
||||||
2: 89,
|
return getUnixStartOfDay(fromDate);
|
||||||
3: 179,
|
|
||||||
4: 364,
|
|
||||||
};
|
|
||||||
const diff = dateRange[this.currentDateRangeSelection.id];
|
|
||||||
const fromDate = subDays(new Date(), diff);
|
|
||||||
return this.fromCustomDate(fromDate);
|
|
||||||
},
|
},
|
||||||
groupBy() {
|
validGroupOptions() {
|
||||||
if (this.isDateRangeSelected) {
|
return this.selectedDateRange.groupByOptions;
|
||||||
return GROUP_BY_FILTER[4].period;
|
},
|
||||||
|
validGroupBy() {
|
||||||
|
if (!this.selectedGroupByFilter) {
|
||||||
|
return this.validGroupOptions[0];
|
||||||
}
|
}
|
||||||
const groupRange = {
|
|
||||||
0: GROUP_BY_FILTER[1].period,
|
const validIds = this.validGroupOptions.map(opt => opt.id);
|
||||||
1: GROUP_BY_FILTER[2].period,
|
if (validIds.includes(this.selectedGroupByFilter.id)) {
|
||||||
2: GROUP_BY_FILTER[3].period,
|
return this.selectedGroupByFilter;
|
||||||
3: GROUP_BY_FILTER[3].period,
|
}
|
||||||
4: GROUP_BY_FILTER[3].period,
|
return this.validGroupOptions[0];
|
||||||
};
|
|
||||||
return groupRange[this.currentDateRangeSelection.id];
|
|
||||||
},
|
|
||||||
notLast7Days() {
|
|
||||||
return this.groupBy !== GROUP_BY_FILTER[1].period;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
filterItemsList() {
|
|
||||||
this.currentSelectedFilter = this.selectedGroupByFilter;
|
|
||||||
},
|
|
||||||
businessHoursSelected() {
|
businessHoursSelected() {
|
||||||
this.$emit('business-hours-toggle', this.businessHoursSelected);
|
this.emitChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.onDateRangeChange();
|
this.emitChange();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onDateRangeChange() {
|
emitChange() {
|
||||||
this.$emit('date-range-change', {
|
const {
|
||||||
from: this.from,
|
from,
|
||||||
to: this.to,
|
to,
|
||||||
groupBy: this.groupBy,
|
selectedGroupByFilter: groupBy,
|
||||||
|
businessHoursSelected: businessHours,
|
||||||
|
selectedAgents,
|
||||||
|
selectedLabel,
|
||||||
|
selectedInbox,
|
||||||
|
selectedTeam,
|
||||||
|
selectedRating,
|
||||||
|
} = this;
|
||||||
|
this.$emit('filter-change', {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
groupBy,
|
||||||
|
businessHours,
|
||||||
|
selectedAgents,
|
||||||
|
selectedLabel,
|
||||||
|
selectedInbox,
|
||||||
|
selectedTeam,
|
||||||
|
selectedRating,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fromCustomDate(date) {
|
onDateRangeChange(selectedRange) {
|
||||||
return getUnixTime(startOfDay(date));
|
this.selectedDateRange = selectedRange;
|
||||||
|
this.selectedGroupByFilter = this.validGroupBy;
|
||||||
|
this.emitChange();
|
||||||
},
|
},
|
||||||
toCustomDate(date) {
|
onCustomDateRangeChange(value) {
|
||||||
return getUnixTime(endOfDay(date));
|
|
||||||
},
|
|
||||||
changeDateSelection(selectedRange) {
|
|
||||||
this.currentDateRangeSelection = selectedRange;
|
|
||||||
this.onDateRangeChange();
|
|
||||||
},
|
|
||||||
onChange(value) {
|
|
||||||
this.customDateRange = value;
|
this.customDateRange = value;
|
||||||
this.onDateRangeChange();
|
this.selectedGroupByFilter = this.validGroupBy;
|
||||||
|
this.emitChange();
|
||||||
},
|
},
|
||||||
changeFilterSelection() {
|
onGroupingChange(payload) {
|
||||||
this.$emit('filter-change', this.currentSelectedFilter);
|
this.selectedGroupByFilter = payload;
|
||||||
|
this.emitChange();
|
||||||
},
|
},
|
||||||
handleAgentsFilterSelection() {
|
handleAgentsFilterSelection(selectedAgents) {
|
||||||
this.$emit('agents-filter-change', this.selectedAgents);
|
this.selectedAgents = selectedAgents;
|
||||||
|
this.emitChange();
|
||||||
|
},
|
||||||
|
handleLabelsFilterSelection(selectedLabel) {
|
||||||
|
this.selectedLabel = selectedLabel;
|
||||||
|
this.emitChange();
|
||||||
|
},
|
||||||
|
handleInboxFilterSelection(selectedInbox) {
|
||||||
|
this.selectedInbox = selectedInbox;
|
||||||
|
this.emitChange();
|
||||||
|
},
|
||||||
|
handleTeamFilterSelection(selectedTeam) {
|
||||||
|
this.selectedTeam = selectedTeam;
|
||||||
|
this.emitChange();
|
||||||
|
},
|
||||||
|
handleRatingFilterSelection(selectedRating) {
|
||||||
|
this.selectedRating = selectedRating;
|
||||||
|
this.emitChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.filter-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
grid-gap: var(--space-slab);
|
||||||
|
|
||||||
|
margin-bottom: var(--space-normal);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div class="multiselect-wrap--small">
|
||||||
|
<multiselect
|
||||||
|
v-model="selectedOptions"
|
||||||
|
class="no-margin"
|
||||||
|
:options="options"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:multiple="true"
|
||||||
|
:close-on-select="false"
|
||||||
|
:clear-on-select="false"
|
||||||
|
:hide-selected="true"
|
||||||
|
:placeholder="$t('CSAT_REPORTS.FILTERS.AGENTS.PLACEHOLDER')"
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
:deselect-label="$t('FORMS.MULTISELECT.ENTER_TO_REMOVE')"
|
||||||
|
@input="handleInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ReportsFiltersAgents',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedOptions: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
options: 'agents/getAgents',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('agents/get');
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleInput() {
|
||||||
|
this.$emit('agents-filter-selection', this.selectedOptions);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div class="multiselect-wrap--small">
|
||||||
|
<p aria-hidden="true" class="hide">
|
||||||
|
{{ $t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL') }}
|
||||||
|
</p>
|
||||||
|
<multiselect
|
||||||
|
v-model="currentSelectedFilter"
|
||||||
|
class="no-margin"
|
||||||
|
track-by="id"
|
||||||
|
label="groupBy"
|
||||||
|
:placeholder="$t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL')"
|
||||||
|
:options="translatedOptions"
|
||||||
|
:allow-empty="false"
|
||||||
|
:show-labels="false"
|
||||||
|
@select="changeFilterSelection"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { GROUP_BY_OPTIONS } from '../../constants';
|
||||||
|
|
||||||
|
const EVENT_NAME = 'on-grouping-change';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ReportsFiltersDateGroupBy',
|
||||||
|
props: {
|
||||||
|
validGroupOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [GROUP_BY_OPTIONS.DAY],
|
||||||
|
},
|
||||||
|
selectedOption: {
|
||||||
|
type: Object,
|
||||||
|
default: () => GROUP_BY_OPTIONS.DAY,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentSelectedFilter: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
translatedOptions() {
|
||||||
|
return this.validGroupOptions.map(option => ({
|
||||||
|
...option,
|
||||||
|
groupBy: this.$t(option.translationKey),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
selectedOption: {
|
||||||
|
handler() {
|
||||||
|
this.currentSelectedFilter = {
|
||||||
|
...this.selectedOption,
|
||||||
|
groupBy: this.$t(this.selectedOption.translationKey),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeFilterSelection(selectedFilter) {
|
||||||
|
this.groupByOptions = this.$emit(EVENT_NAME, selectedFilter);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div class="multiselect-wrap--small">
|
||||||
|
<multiselect
|
||||||
|
v-model="selectedOption"
|
||||||
|
class="no-margin"
|
||||||
|
track-by="name"
|
||||||
|
label="name"
|
||||||
|
:placeholder="$t('FORMS.MULTISELECT.SELECT_ONE')"
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
deselect-label=""
|
||||||
|
:options="options"
|
||||||
|
:searchable="false"
|
||||||
|
:allow-empty="false"
|
||||||
|
@select="updateRange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { DATE_RANGE_OPTIONS } from '../../constants';
|
||||||
|
|
||||||
|
const EVENT_NAME = 'on-range-change';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ReportFiltersDateRange',
|
||||||
|
data() {
|
||||||
|
const translatedOptions = Object.values(DATE_RANGE_OPTIONS).map(option => ({
|
||||||
|
...option,
|
||||||
|
name: this.$t(option.translationKey),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
// relies on translations, need to move it to constants
|
||||||
|
selectedOption: translatedOptions[0],
|
||||||
|
options: translatedOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateRange(selectedRange) {
|
||||||
|
this.selectedOption = selectedRange;
|
||||||
|
this.$emit(EVENT_NAME, selectedRange);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="multiselect-wrap--small">
|
||||||
|
<multiselect
|
||||||
|
v-model="selectedOption"
|
||||||
|
class="no-margin"
|
||||||
|
:placeholder="$t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL')"
|
||||||
|
label="name"
|
||||||
|
track-by="id"
|
||||||
|
:options="options"
|
||||||
|
:option-height="24"
|
||||||
|
:show-labels="false"
|
||||||
|
@input="handleInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ReportsFiltersInboxes',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedOption: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
options: 'inboxes/getInboxes',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('inboxes/get');
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleInput() {
|
||||||
|
this.$emit('inbox-filter-selection', this.selectedOption);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div class="multiselect-wrap--small">
|
||||||
|
<multiselect
|
||||||
|
v-model="selectedOption"
|
||||||
|
class="no-margin"
|
||||||
|
:placeholder="$t('LABEL_REPORTS.FILTER_DROPDOWN_LABEL')"
|
||||||
|
label="title"
|
||||||
|
track-by="id"
|
||||||
|
:options="options"
|
||||||
|
:option-height="24"
|
||||||
|
:show-labels="false"
|
||||||
|
@input="handleInput"
|
||||||
|
>
|
||||||
|
<template slot="singleLabel" slot-scope="props">
|
||||||
|
<div class="reports-option__wrap">
|
||||||
|
<div
|
||||||
|
:style="{ backgroundColor: props.option.color }"
|
||||||
|
class="reports-option__rounded--item"
|
||||||
|
/>
|
||||||
|
<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="reports-option__wrap">
|
||||||
|
<div
|
||||||
|
:style="{ backgroundColor: props.option.color }"
|
||||||
|
class="
|
||||||
|
reports-option__rounded--item
|
||||||
|
reports-option__item
|
||||||
|
reports-option__label--swatch
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<span class="reports-option__desc">
|
||||||
|
<span class="reports-option__title">
|
||||||
|
{{ props.option.title }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</multiselect>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ReportsFiltersLabels',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedOption: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
options: 'labels/getLabels',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('labels/get');
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleInput() {
|
||||||
|
this.$emit('labels-filter-selection', this.selectedOption);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<div class="multiselect-wrap--small">
|
||||||
|
<multiselect
|
||||||
|
v-model="selectedOption"
|
||||||
|
class="no-margin"
|
||||||
|
:option-height="24"
|
||||||
|
:placeholder="$t('FORMS.MULTISELECT.SELECT_ONE')"
|
||||||
|
:options="options"
|
||||||
|
:show-labels="false"
|
||||||
|
track-by="value"
|
||||||
|
label="label"
|
||||||
|
@input="handleInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ReportFiltersRatings',
|
||||||
|
data() {
|
||||||
|
const translatedOptions = CSAT_RATINGS.reverse().map(option => ({
|
||||||
|
...option,
|
||||||
|
label: this.$t(option.translationKey),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedOption: null,
|
||||||
|
options: translatedOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleInput(selectedRating) {
|
||||||
|
this.$emit('rating-filter-selection', selectedRating);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="multiselect-wrap--small">
|
||||||
|
<multiselect
|
||||||
|
v-model="selectedOption"
|
||||||
|
class="no-margin"
|
||||||
|
:placeholder="$t('TEAM_REPORTS.FILTER_DROPDOWN_LABEL')"
|
||||||
|
label="name"
|
||||||
|
track-by="id"
|
||||||
|
:options="options"
|
||||||
|
:option-height="24"
|
||||||
|
:show-labels="false"
|
||||||
|
@input="handleInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ReportsFiltersTeams',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedOption: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
options: 'teams/getTeams',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('teams/get');
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleInput() {
|
||||||
|
this.$emit('team-filter-selection', this.selectedOption);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
|
import Vuex from 'vuex';
|
||||||
|
import ReportsFiltersAgents from '../../Filters/Agents';
|
||||||
|
|
||||||
|
const localVue = createLocalVue();
|
||||||
|
localVue.use(Vuex);
|
||||||
|
|
||||||
|
const mockStore = new Vuex.Store({
|
||||||
|
modules: {
|
||||||
|
agents: {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
agents: [],
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
getAgents: state => state.agents,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
get: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mountParams = {
|
||||||
|
localVue,
|
||||||
|
store: mockStore,
|
||||||
|
mocks: {
|
||||||
|
$t: msg => msg,
|
||||||
|
},
|
||||||
|
stubs: ['multiselect'],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ReportsFiltersAgents.vue', () => {
|
||||||
|
it('emits "agents-filter-selection" event when handleInput is called', () => {
|
||||||
|
const wrapper = shallowMount(ReportsFiltersAgents, mountParams);
|
||||||
|
|
||||||
|
const selectedAgents = [
|
||||||
|
{ id: 1, name: 'Agent 1' },
|
||||||
|
{ id: 2, name: 'Agent 2' },
|
||||||
|
];
|
||||||
|
wrapper.setData({ selectedOptions: selectedAgents });
|
||||||
|
|
||||||
|
wrapper.vm.handleInput();
|
||||||
|
|
||||||
|
expect(wrapper.emitted('agents-filter-selection')).toBeTruthy();
|
||||||
|
expect(wrapper.emitted('agents-filter-selection')[0]).toEqual([
|
||||||
|
selectedAgents,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches the "agents/get" action when the component is mounted', () => {
|
||||||
|
const dispatchSpy = jest.spyOn(mockStore, 'dispatch');
|
||||||
|
|
||||||
|
shallowMount(ReportsFiltersAgents, mountParams);
|
||||||
|
|
||||||
|
expect(dispatchSpy).toHaveBeenCalledWith('agents/get');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import ReportsFiltersDateGroupBy from '../../Filters/DateGroupBy';
|
||||||
|
import { GROUP_BY_OPTIONS } from '../../../constants';
|
||||||
|
|
||||||
|
const mountParams = {
|
||||||
|
mocks: {
|
||||||
|
$t: msg => msg,
|
||||||
|
},
|
||||||
|
stubs: ['multiselect'],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ReportsFiltersDateGroupBy.vue', () => {
|
||||||
|
it('emits "on-grouping-change" event when changeFilterSelection is called', () => {
|
||||||
|
const wrapper = shallowMount(ReportsFiltersDateGroupBy, mountParams);
|
||||||
|
|
||||||
|
const selectedFilter = GROUP_BY_OPTIONS.DAY;
|
||||||
|
wrapper.vm.changeFilterSelection(selectedFilter);
|
||||||
|
|
||||||
|
expect(wrapper.emitted('on-grouping-change')).toBeTruthy();
|
||||||
|
expect(wrapper.emitted('on-grouping-change')[0]).toEqual([selectedFilter]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates currentSelectedFilter when selectedOption is changed', async () => {
|
||||||
|
const wrapper = shallowMount(ReportsFiltersDateGroupBy, mountParams);
|
||||||
|
|
||||||
|
const newSelectedOption = GROUP_BY_OPTIONS.MONTH;
|
||||||
|
await wrapper.setProps({ selectedOption: newSelectedOption });
|
||||||
|
|
||||||
|
expect(wrapper.vm.currentSelectedFilter).toEqual({
|
||||||
|
...newSelectedOption,
|
||||||
|
groupBy: newSelectedOption.translationKey,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes translatedOptions correctly', () => {
|
||||||
|
const wrapper = shallowMount(ReportsFiltersDateGroupBy, mountParams);
|
||||||
|
|
||||||
|
const expectedOptions = wrapper.vm.validGroupOptions.map(option => ({
|
||||||
|
...option,
|
||||||
|
groupBy: option.translationKey,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(wrapper.vm.translatedOptions).toEqual(expectedOptions);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import ReportFiltersDateRange from '../../Filters/DateRange';
|
||||||
|
import { DATE_RANGE_OPTIONS } from '../../../constants';
|
||||||
|
|
||||||
|
const mountParams = {
|
||||||
|
mocks: {
|
||||||
|
$t: msg => msg,
|
||||||
|
},
|
||||||
|
stubs: ['multiselect'],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ReportFiltersDateRange.vue', () => {
|
||||||
|
it('emits "on-range-change" event when updateRange is called', () => {
|
||||||
|
const wrapper = shallowMount(ReportFiltersDateRange, mountParams);
|
||||||
|
|
||||||
|
const selectedRange = DATE_RANGE_OPTIONS.LAST_7_DAYS;
|
||||||
|
wrapper.vm.updateRange(selectedRange);
|
||||||
|
|
||||||
|
expect(wrapper.emitted('on-range-change')).toBeTruthy();
|
||||||
|
expect(wrapper.emitted('on-range-change')[0]).toEqual([selectedRange]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes options correctly', () => {
|
||||||
|
const wrapper = shallowMount(ReportFiltersDateRange, mountParams);
|
||||||
|
|
||||||
|
const expectedOptions = Object.values(DATE_RANGE_OPTIONS).map(option => ({
|
||||||
|
...option,
|
||||||
|
name: option.translationKey,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(wrapper.vm.options).toEqual(expectedOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes selectedOption correctly', () => {
|
||||||
|
const wrapper = shallowMount(ReportFiltersDateRange, mountParams);
|
||||||
|
const expectedSelectedOption = Object.values(DATE_RANGE_OPTIONS)[0];
|
||||||
|
expect(wrapper.vm.selectedOption).toEqual({
|
||||||
|
...expectedSelectedOption,
|
||||||
|
name: expectedSelectedOption.translationKey,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
|
import Vuex from 'vuex';
|
||||||
|
import ReportsFiltersInboxes from '../../Filters/Inboxes';
|
||||||
|
|
||||||
|
const localVue = createLocalVue();
|
||||||
|
localVue.use(Vuex);
|
||||||
|
|
||||||
|
const mountParams = {
|
||||||
|
mocks: {
|
||||||
|
$t: msg => msg,
|
||||||
|
},
|
||||||
|
stubs: ['multiselect'],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ReportsFiltersInboxes.vue', () => {
|
||||||
|
let store;
|
||||||
|
let inboxesModule;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
inboxesModule = {
|
||||||
|
namespaced: true,
|
||||||
|
getters: {
|
||||||
|
getInboxes: () => () => [
|
||||||
|
{ id: 1, name: 'Inbox 1' },
|
||||||
|
{ id: 2, name: 'Inbox 2' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
get: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
store = new Vuex.Store({
|
||||||
|
modules: {
|
||||||
|
inboxes: inboxesModule,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches "inboxes/get" action when component is mounted', () => {
|
||||||
|
shallowMount(ReportsFiltersInboxes, {
|
||||||
|
store,
|
||||||
|
localVue,
|
||||||
|
...mountParams,
|
||||||
|
});
|
||||||
|
expect(inboxesModule.actions.get).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits "inbox-filter-selection" event when handleInput is called', () => {
|
||||||
|
const wrapper = shallowMount(ReportsFiltersInboxes, {
|
||||||
|
store,
|
||||||
|
localVue,
|
||||||
|
...mountParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedInbox = { id: 1, name: 'Inbox 1' };
|
||||||
|
wrapper.setData({ selectedOption: selectedInbox });
|
||||||
|
|
||||||
|
wrapper.vm.handleInput();
|
||||||
|
|
||||||
|
expect(wrapper.emitted('inbox-filter-selection')).toBeTruthy();
|
||||||
|
expect(wrapper.emitted('inbox-filter-selection')[0]).toEqual([
|
||||||
|
selectedInbox,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
|
import Vuex from 'vuex';
|
||||||
|
import ReportsFiltersLabels from '../../Filters/Labels';
|
||||||
|
|
||||||
|
const localVue = createLocalVue();
|
||||||
|
localVue.use(Vuex);
|
||||||
|
|
||||||
|
const mountParams = {
|
||||||
|
mocks: {
|
||||||
|
$t: msg => msg,
|
||||||
|
},
|
||||||
|
stubs: ['multiselect'],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ReportsFiltersLabels.vue', () => {
|
||||||
|
let store;
|
||||||
|
let labelsModule;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
labelsModule = {
|
||||||
|
namespaced: true,
|
||||||
|
getters: {
|
||||||
|
getLabels: () => () => [
|
||||||
|
{ id: 1, title: 'Label 1', color: 'red' },
|
||||||
|
{ id: 2, title: 'Label 2', color: 'blue' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
get: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
store = new Vuex.Store({
|
||||||
|
modules: {
|
||||||
|
labels: labelsModule,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches "labels/get" action when component is mounted', () => {
|
||||||
|
shallowMount(ReportsFiltersLabels, {
|
||||||
|
store,
|
||||||
|
localVue,
|
||||||
|
...mountParams,
|
||||||
|
});
|
||||||
|
expect(labelsModule.actions.get).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits "labels-filter-selection" event when handleInput is called', () => {
|
||||||
|
const wrapper = shallowMount(ReportsFiltersLabels, {
|
||||||
|
store,
|
||||||
|
localVue,
|
||||||
|
...mountParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedLabel = { id: 1, title: 'Label 1', color: 'red' };
|
||||||
|
wrapper.setData({ selectedOption: selectedLabel });
|
||||||
|
|
||||||
|
wrapper.vm.handleInput();
|
||||||
|
|
||||||
|
expect(wrapper.emitted('labels-filter-selection')).toBeTruthy();
|
||||||
|
expect(wrapper.emitted('labels-filter-selection')[0]).toEqual([
|
||||||
|
selectedLabel,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
|
import ReportFiltersRatings from '../../Filters/Ratings';
|
||||||
|
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||||
|
|
||||||
|
const mountParams = {
|
||||||
|
mocks: {
|
||||||
|
$t: msg => msg,
|
||||||
|
},
|
||||||
|
stubs: ['multiselect'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const localVue = createLocalVue();
|
||||||
|
|
||||||
|
describe('ReportFiltersRatings.vue', () => {
|
||||||
|
it('emits "rating-filter-selection" event when handleInput is called', () => {
|
||||||
|
const wrapper = shallowMount(ReportFiltersRatings, {
|
||||||
|
localVue,
|
||||||
|
...mountParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedRating = { value: 1, label: 'Rating 1' };
|
||||||
|
wrapper.setData({ selectedOption: selectedRating });
|
||||||
|
|
||||||
|
wrapper.vm.handleInput(selectedRating);
|
||||||
|
|
||||||
|
expect(wrapper.emitted('rating-filter-selection')).toBeTruthy();
|
||||||
|
expect(wrapper.emitted('rating-filter-selection')[0]).toEqual([
|
||||||
|
selectedRating,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes options correctly', () => {
|
||||||
|
const wrapper = shallowMount(ReportFiltersRatings, {
|
||||||
|
localVue,
|
||||||
|
...mountParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedOptions = CSAT_RATINGS.map(option => ({
|
||||||
|
...option,
|
||||||
|
label: option.translationKey,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(wrapper.vm.options).toEqual(expectedOptions);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
|
import Vuex from 'vuex';
|
||||||
|
import ReportsFiltersTeams from '../../Filters/Teams.vue';
|
||||||
|
|
||||||
|
const localVue = createLocalVue();
|
||||||
|
localVue.use(Vuex);
|
||||||
|
|
||||||
|
const mountParams = {
|
||||||
|
mocks: {
|
||||||
|
$t: msg => msg,
|
||||||
|
},
|
||||||
|
stubs: ['multiselect'],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ReportsFiltersTeams.vue', () => {
|
||||||
|
let store;
|
||||||
|
let teamsModule;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
teamsModule = {
|
||||||
|
namespaced: true,
|
||||||
|
getters: {
|
||||||
|
getTeams: () => () => [
|
||||||
|
{ id: 1, name: 'Team 1' },
|
||||||
|
{ id: 2, name: 'Team 2' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
get: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
store = new Vuex.Store({
|
||||||
|
modules: {
|
||||||
|
teams: teamsModule,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches "teams/get" action when component is mounted', () => {
|
||||||
|
shallowMount(ReportsFiltersTeams, {
|
||||||
|
store,
|
||||||
|
localVue,
|
||||||
|
...mountParams,
|
||||||
|
});
|
||||||
|
expect(teamsModule.actions.get).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits "team-filter-selection" event when handleInput is called', () => {
|
||||||
|
const wrapper = shallowMount(ReportsFiltersTeams, {
|
||||||
|
store,
|
||||||
|
localVue,
|
||||||
|
...mountParams,
|
||||||
|
});
|
||||||
|
wrapper.setData({ selectedOption: { id: 1, name: 'Team 1' } });
|
||||||
|
wrapper.vm.handleInput();
|
||||||
|
expect(wrapper.emitted('team-filter-selection')).toBeTruthy();
|
||||||
|
expect(wrapper.emitted('team-filter-selection')[0]).toEqual([
|
||||||
|
{ id: 1, name: 'Team 1' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,6 +7,85 @@ export const GROUP_BY_FILTER = {
|
|||||||
4: { id: 4, period: 'year' },
|
4: { id: 4, period: 'year' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const GROUP_BY_OPTIONS = {
|
||||||
|
DAY: {
|
||||||
|
id: 'DAY',
|
||||||
|
period: 'day',
|
||||||
|
translationKey: 'REPORT.GROUPING_OPTIONS.DAY',
|
||||||
|
},
|
||||||
|
WEEK: {
|
||||||
|
id: 'WEEK',
|
||||||
|
period: 'week',
|
||||||
|
translationKey: 'REPORT.GROUPING_OPTIONS.WEEK',
|
||||||
|
},
|
||||||
|
MONTH: {
|
||||||
|
id: 'MONTH',
|
||||||
|
period: 'month',
|
||||||
|
translationKey: 'REPORT.GROUPING_OPTIONS.MONTH',
|
||||||
|
},
|
||||||
|
YEAR: {
|
||||||
|
id: 'YEAR',
|
||||||
|
period: 'year',
|
||||||
|
translationKey: 'REPORT.GROUPING_OPTIONS.YEAR',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DATE_RANGE_OPTIONS = {
|
||||||
|
LAST_7_DAYS: {
|
||||||
|
id: 'LAST_7_DAYS',
|
||||||
|
translationKey: 'REPORT.DATE_RANGE_OPTIONS.LAST_7_DAYS',
|
||||||
|
offset: 6,
|
||||||
|
groupByOptions: [GROUP_BY_OPTIONS.DAY],
|
||||||
|
},
|
||||||
|
LAST_30_DAYS: {
|
||||||
|
id: 'LAST_30_DAYS',
|
||||||
|
translationKey: 'REPORT.DATE_RANGE_OPTIONS.LAST_30_DAYS',
|
||||||
|
offset: 29,
|
||||||
|
groupByOptions: [GROUP_BY_OPTIONS.DAY, GROUP_BY_OPTIONS.WEEK],
|
||||||
|
},
|
||||||
|
LAST_3_MONTHS: {
|
||||||
|
id: 'LAST_3_MONTHS',
|
||||||
|
translationKey: 'REPORT.DATE_RANGE_OPTIONS.LAST_3_MONTHS',
|
||||||
|
offset: 89,
|
||||||
|
groupByOptions: [
|
||||||
|
GROUP_BY_OPTIONS.DAY,
|
||||||
|
GROUP_BY_OPTIONS.WEEK,
|
||||||
|
GROUP_BY_OPTIONS.MONTH,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
LAST_6_MONTHS: {
|
||||||
|
id: 'LAST_6_MONTHS',
|
||||||
|
translationKey: 'REPORT.DATE_RANGE_OPTIONS.LAST_6_MONTHS',
|
||||||
|
offset: 179,
|
||||||
|
groupByOptions: [
|
||||||
|
GROUP_BY_OPTIONS.DAY,
|
||||||
|
GROUP_BY_OPTIONS.WEEK,
|
||||||
|
GROUP_BY_OPTIONS.MONTH,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
LAST_YEAR: {
|
||||||
|
id: 'LAST_YEAR',
|
||||||
|
translationKey: 'REPORT.DATE_RANGE_OPTIONS.LAST_YEAR',
|
||||||
|
offset: 364,
|
||||||
|
groupByOptions: [
|
||||||
|
GROUP_BY_OPTIONS.DAY,
|
||||||
|
GROUP_BY_OPTIONS.WEEK,
|
||||||
|
GROUP_BY_OPTIONS.MONTH,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
CUSTOM_DATE_RANGE: {
|
||||||
|
id: 'CUSTOM_DATE_RANGE',
|
||||||
|
translationKey: 'REPORT.DATE_RANGE_OPTIONS.CUSTOM_DATE_RANGE',
|
||||||
|
offset: null,
|
||||||
|
groupByOptions: [
|
||||||
|
GROUP_BY_OPTIONS.DAY,
|
||||||
|
GROUP_BY_OPTIONS.WEEK,
|
||||||
|
GROUP_BY_OPTIONS.MONTH,
|
||||||
|
GROUP_BY_OPTIONS.YEAR,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const CHART_FONT_FAMILY =
|
export const CHART_FONT_FAMILY =
|
||||||
'-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
'-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
||||||
|
|
||||||
|
|||||||
@@ -85,13 +85,10 @@ export const getters = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
get: async function getResponses(
|
get: async function getResponses({ commit }, params) {
|
||||||
{ commit },
|
|
||||||
{ page = 1, from, to, user_ids } = {}
|
|
||||||
) {
|
|
||||||
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: true });
|
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: true });
|
||||||
try {
|
try {
|
||||||
const response = await CSATReports.get({ page, from, to, user_ids });
|
const response = await CSATReports.get(params);
|
||||||
commit(types.SET_CSAT_RESPONSE, response.data);
|
commit(types.SET_CSAT_RESPONSE, response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore error
|
// Ignore error
|
||||||
@@ -99,10 +96,10 @@ export const actions = {
|
|||||||
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: false });
|
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getMetrics: async function getMetrics({ commit }, { from, to, user_ids }) {
|
getMetrics: async function getMetrics({ commit }, params) {
|
||||||
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: true });
|
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: true });
|
||||||
try {
|
try {
|
||||||
const response = await CSATReports.getMetrics({ from, to, user_ids });
|
const response = await CSATReports.getMetrics(params);
|
||||||
commit(types.SET_CSAT_RESPONSE_METRICS, response.data);
|
commit(types.SET_CSAT_RESPONSE_METRICS, response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore error
|
// Ignore error
|
||||||
|
|||||||
@@ -59,24 +59,28 @@ export const ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP =
|
|||||||
export const CSAT_RATINGS = [
|
export const CSAT_RATINGS = [
|
||||||
{
|
{
|
||||||
key: 'disappointed',
|
key: 'disappointed',
|
||||||
|
translationKey: 'CSAT.RATINGS.POOR',
|
||||||
emoji: '😞',
|
emoji: '😞',
|
||||||
value: 1,
|
value: 1,
|
||||||
color: '#FDAD2A',
|
color: '#FDAD2A',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'expressionless',
|
key: 'expressionless',
|
||||||
|
translationKey: 'CSAT.RATINGS.FAIR',
|
||||||
emoji: '😑',
|
emoji: '😑',
|
||||||
value: 2,
|
value: 2,
|
||||||
color: '#FFC532',
|
color: '#FFC532',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'neutral',
|
key: 'neutral',
|
||||||
|
translationKey: 'CSAT.RATINGS.AVERAGE',
|
||||||
emoji: '😐',
|
emoji: '😐',
|
||||||
value: 3,
|
value: 3,
|
||||||
color: '#FCEC56',
|
color: '#FCEC56',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'grinning',
|
key: 'grinning',
|
||||||
|
translationKey: 'CSAT.RATINGS.GOOD',
|
||||||
emoji: '😀',
|
emoji: '😀',
|
||||||
value: 4,
|
value: 4,
|
||||||
color: '#6FD86F',
|
color: '#6FD86F',
|
||||||
@@ -84,6 +88,7 @@ export const CSAT_RATINGS = [
|
|||||||
{
|
{
|
||||||
key: 'smiling',
|
key: 'smiling',
|
||||||
emoji: '😍',
|
emoji: '😍',
|
||||||
|
translationKey: 'CSAT.RATINGS.EXCELLENT',
|
||||||
value: 5,
|
value: 5,
|
||||||
color: '#44CE4B',
|
color: '#44CE4B',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import fromUnixTime from 'date-fns/fromUnixTime';
|
|||||||
import format from 'date-fns/format';
|
import format from 'date-fns/format';
|
||||||
import isToday from 'date-fns/isToday';
|
import isToday from 'date-fns/isToday';
|
||||||
import isYesterday from 'date-fns/isYesterday';
|
import isYesterday from 'date-fns/isYesterday';
|
||||||
|
import { endOfDay, getUnixTime, startOfDay } from 'date-fns';
|
||||||
|
|
||||||
export const formatUnixDate = (date, dateFormat = 'MMM dd, yyyy') => {
|
export const formatUnixDate = (date, dateFormat = 'MMM dd, yyyy') => {
|
||||||
const unixDate = fromUnixTime(date);
|
const unixDate = fromUnixTime(date);
|
||||||
@@ -31,6 +32,12 @@ export const isTimeAfter = (h1, m1, h2, m2) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Get start of day as a UNIX timestamp */
|
||||||
|
export const getUnixStartOfDay = date => getUnixTime(startOfDay(date));
|
||||||
|
|
||||||
|
/** Get end of day as a UNIX timestamp */
|
||||||
|
export const getUnixEndOfDay = date => getUnixTime(endOfDay(date));
|
||||||
|
|
||||||
export const generateRelativeTime = (value, unit, languageCode) => {
|
export const generateRelativeTime = (value, unit, languageCode) => {
|
||||||
const rtf = new Intl.RelativeTimeFormat(languageCode, {
|
const rtf = new Intl.RelativeTimeFormat(languageCode, {
|
||||||
numeric: 'auto',
|
numeric: 'auto',
|
||||||
|
|||||||
@@ -35,4 +35,8 @@ class CsatSurveyResponse < ApplicationRecord
|
|||||||
|
|
||||||
scope :filter_by_created_at, ->(range) { where(created_at: range) if range.present? }
|
scope :filter_by_created_at, ->(range) { where(created_at: range) if range.present? }
|
||||||
scope :filter_by_assigned_agent_id, ->(user_ids) { where(assigned_agent_id: user_ids) if user_ids.present? }
|
scope :filter_by_assigned_agent_id, ->(user_ids) { where(assigned_agent_id: user_ids) if user_ids.present? }
|
||||||
|
scope :filter_by_inbox_id, ->(inbox_id) { joins(:conversation).where(conversations: { inbox_id: inbox_id }) if inbox_id.present? }
|
||||||
|
scope :filter_by_team_id, ->(team_id) { joins(:conversation).where(conversations: { team_id: team_id }) if team_id.present? }
|
||||||
|
# filter by rating value
|
||||||
|
scope :filter_by_rating, ->(rating) { where(rating: rating) if rating.present? }
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user