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:
Shivam Mishra
2023-05-18 22:50:46 +05:30
committed by GitHub
parent 132cf802d5
commit 105f9a27d2
29 changed files with 1150 additions and 256 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 },
}); });
} }
} }

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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"
}
} }
} }

View File

@@ -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"

View File

@@ -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>

View File

@@ -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',
}); });
}, },

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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');
});
});

View File

@@ -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);
});
});

View File

@@ -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,
});
});
});

View File

@@ -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,
]);
});
});

View File

@@ -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,
]);
});
});

View File

@@ -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);
});
});

View File

@@ -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' },
]);
});
});

View File

@@ -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';

View File

@@ -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

View File

@@ -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',
}, },

View File

@@ -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',

View File

@@ -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