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
arm64-darwin-20
arm64-darwin-22
arm64-darwin-21
ruby
x86_64-darwin-18

View File

@@ -34,9 +34,12 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
end
def set_csat_survey_responses
@csat_survey_responses = filtrate(
Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
).filter_by_created_at(range).filter_by_assigned_agent_id(params[:user_ids])
base_query = Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
@csat_survey_responses = filtrate(base_query).filter_by_created_at(range)
.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
def set_current_page_surveys

View File

@@ -6,7 +6,7 @@ class CSATReportsAPI extends ApiClient {
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, {
params: {
page,
@@ -14,24 +14,31 @@ class CSATReportsAPI extends ApiClient {
until: to,
sort: '-created_at',
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`, {
params: {
since: from,
until: to,
sort: '-created_at',
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`, {
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 {
.mx-datepicker {
width: 100%;
&.no-margin {
.mx-input {
margin-bottom: 0;
}
}
.mx-datepicker-range {
width: 320px;
&:not(.auto-width) {
.mx-datepicker-range {
width: 320px;
}
}
.mx-datepicker {
width: 100%;
}
.mx-input {

View File

@@ -13,7 +13,9 @@
}
.multiselect {
margin-bottom: var(--space-normal);
&:not(.no-margin) {
margin-bottom: var(--space-normal);
}
&.multiselect--disabled {
opacity: 0.8;

View File

@@ -1,6 +1,13 @@
{
"CSAT": {
"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...",
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
"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": {
"CONVERSATIONS": {
"NAME": "Conversations",
@@ -34,6 +36,14 @@
"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": [
{
"id": 0,
@@ -66,6 +76,12 @@
},
"GROUP_BY_FILTER_DROPDOWN_LABEL": "Group By",
"DURATION_FILTER_LABEL": "Duration",
"GROUPING_OPTIONS": {
"DAY": "Day",
"WEEK": "Week",
"MONTH": "Month",
"YEAR": "Year"
},
"GROUP_BY_DAY_OPTIONS": [{ "id": 1, "groupBy": "Day" }],
"GROUP_BY_WEEK_OPTIONS": [
{ "id": 1, "groupBy": "Day" },
@@ -356,6 +372,7 @@
"HEADER": "CSAT Reports",
"NO_RECORDS": "There are no CSAT survey responses available.",
"DOWNLOAD": "Download CSAT Reports",
"DOWNLOAD_FAILED": "Failed to download CSAT Reports",
"FILTERS": {
"AGENTS": {
"PLACEHOLDER": "Choose Agents"

View File

@@ -1,11 +1,12 @@
<template>
<div class="column content-box">
<report-filter-selector
agents-filter
:agents-filter-items-list="agentList"
:show-agents-filter="true"
:show-inbox-filter="true"
:show-rating-filter="true"
:show-team-filter="isTeamsEnabled"
:show-business-hours-switch="false"
@date-range-change="onDateRangeChange"
@agents-filter-change="onAgentsFilterChange"
@filter-change="onFilterChange"
/>
<woot-button
color-scheme="success"
@@ -23,9 +24,11 @@
import CsatMetrics from './components/CsatMetrics';
import CsatTable from './components/CsatTable';
import ReportFilterSelector from './components/FilterSelector';
import { mapGetters } from 'vuex';
import { generateFileName } from '../../../../helper/downloadHelper';
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import { mapGetters } from 'vuex';
import { FEATURE_FLAGS } from '../../../../featureFlags';
import alertMixin from '../../../../../shared/mixins/alertMixin';
export default {
name: 'CsatResponses',
@@ -34,39 +37,78 @@ export default {
CsatTable,
ReportFilterSelector,
},
mixins: [alertMixin],
data() {
return { pageIndex: 1, from: 0, to: 0, userIds: [] };
return {
pageIndex: 1,
from: 0,
to: 0,
userIds: [],
inbox: null,
team: null,
rating: null,
};
},
computed: {
...mapGetters({
agentList: 'agents/getAgents',
accountId: 'getCurrentAccountId',
isFeatureEnabledOnAccount: 'accounts/isFeatureEnabledonAccount',
}),
},
mounted() {
this.$store.dispatch('agents/get');
},
methods: {
getAllData() {
this.$store.dispatch('csat/getMetrics', {
requestPayload() {
return {
from: this.from,
to: this.to,
user_ids: this.userIds,
});
this.getResponses();
inbox_id: this.inbox,
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() {
this.$store.dispatch('csat/get', {
page: this.pageIndex,
from: this.from,
to: this.to,
user_ids: this.userIds,
...this.requestPayload,
});
},
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) {
this.pageIndex = pageIndex;
this.getResponses();
},
onDateRangeChange({ from, to }) {
onFilterChange({
from,
to,
selectedAgents,
selectedInbox,
selectedTeam,
selectedRating,
}) {
// do not track filter change on inital load
if (this.from !== 0 && this.to !== 0) {
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
@@ -74,27 +116,16 @@ export default {
reportType: 'csat',
});
}
this.from = from;
this.to = to;
this.userIds = selectedAgents.map(el => el.id);
this.inbox = selectedInbox?.id;
this.team = selectedTeam?.id;
this.rating = selectedRating?.value;
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>

View File

@@ -9,12 +9,9 @@
{{ $t('REPORT.DOWNLOAD_AGENT_REPORTS') }}
</woot-button>
<report-filter-selector
group-by-filter
:selected-group-by-filter="selectedGroupByFilter"
:filter-items-list="filterItemsList"
@date-range-change="onDateRangeChange"
:show-agents-filter="false"
:show-group-by-filter="true"
@filter-change="onFilterChange"
@business-hours-toggle="onBusinessHoursToggle"
/>
<div class="row">
<woot-report-stats-card
@@ -55,7 +52,8 @@ import fromUnixTime from 'date-fns/fromUnixTime';
import format from 'date-fns/format';
import ReportFilterSelector from './components/FilterSelector';
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 { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
@@ -73,15 +71,13 @@ export default {
components: {
ReportFilterSelector,
},
mixins: [reportMixin],
mixins: [reportMixin, alertMixin],
data() {
return {
from: 0,
to: 0,
currentSelection: 0,
groupBy: GROUP_BY_FILTER[1],
filterItemsList: this.$t('REPORT.GROUP_BY_DAY_OPTIONS'),
selectedGroupByFilter: {},
businessHours: false,
};
},
@@ -191,24 +187,35 @@ export default {
},
methods: {
fetchAllData() {
const { from, to, groupBy, businessHours } = this;
this.$store.dispatch('fetchAccountSummary', {
from,
to,
groupBy: groupBy.period,
businessHours,
});
this.fetchAccountSummary();
this.fetchChartData();
},
fetchAccountSummary() {
try {
this.$store.dispatch('fetchAccountSummary', this.getRequestPayload());
} catch {
this.showAlert(this.$t('REPORT.SUMMARY_FETCHING_FAILED'));
}
},
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;
this.$store.dispatch('fetchAccountReport', {
metric: this.metrics[this.currentSelection].KEY,
return {
from,
to,
groupBy: groupBy.period,
businessHours,
});
};
},
downloadAgentReports() {
const { from, to } = this;
@@ -222,57 +229,15 @@ export default {
this.currentSelection = index;
this.fetchChartData();
},
onDateRangeChange({ from, to, groupBy }) {
// 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',
});
}
onFilterChange({ from, to, groupBy, businessHours }) {
this.from = from;
this.to = to;
this.filterItemsList = this.fetchFilterItems(groupBy);
const filterItems = this.filterItemsList.filter(
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.groupBy = groupBy;
this.businessHours = businessHours;
this.fetchAllData();
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
filterType: 'groupBy',
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,
filterValue: { from, to, groupBy, businessHours },
reportType: 'conversations',
});
},

View File

@@ -1,72 +1,43 @@
<template>
<div class="flex-container flex-dir-column medium-flex-dir-row">
<div class="small-12 medium-3 pull-right multiselect-wrap--small">
<multiselect
v-model="currentDateRangeSelection"
track-by="name"
label="name"
:placeholder="$t('FORMS.MULTISELECT.SELECT_ONE')"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:options="dateRange"
:searchable="false"
:allow-empty="false"
@select="changeDateSelection"
/>
</div>
<div class="filter-container">
<reports-filters-date-range @on-range-change="onDateRangeChange" />
<woot-date-range-picker
v-if="isDateRangeSelected"
class="margin-left-1"
show-range
class="no-margin auto-width"
:value="customDateRange"
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
@change="onChange"
@change="onCustomDateRangeChange"
/>
<div
v-if="notLast7Days && groupByFilter"
class="small-12 medium-3 pull-right margin-left-1 margin-right-1 multiselect-wrap--small"
>
<p aria-hidden="true" class="hide">
{{ $t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL') }}
</p>
<multiselect
v-model="currentSelectedFilter"
track-by="id"
label="groupBy"
:placeholder="$t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL')"
:options="filterItemsList"
:allow-empty="false"
:show-labels="false"
@input="changeFilterSelection"
/>
</div>
<div
v-if="agentsFilter"
class="small-12 medium-3 pull-right margin-left-1 margin-right-1 multiselect-wrap--small"
>
<multiselect
v-model="selectedAgents"
:options="agentsFilterItemsList"
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="handleAgentsFilterSelection"
/>
</div>
<div
v-if="showBusinessHoursSwitch"
class="small-12 medium-3 business-hours"
>
<span class="business-hours-text margin-right-1">
<reports-filters-date-group-by
v-if="showGroupByFilter && isGroupByPossible"
:valid-group-options="validGroupOptions"
:selected-option="selectedGroupByFilter"
@on-grouping-change="onGroupingChange"
/>
<reports-filters-agents
v-if="showAgentsFilter"
@agents-filter-selection="handleAgentsFilterSelection"
/>
<reports-filters-labels
v-if="showLabelsFilter"
@labels-filter-selection="handleLabelsFilterSelection"
/>
<reports-filters-teams
v-if="showTeamFilter"
@team-filter-selection="handleTeamFilterSelection"
/>
<reports-filters-inboxes
v-if="showInboxFilter"
@inbox-filter-selection="handleInboxFilterSelection"
/>
<reports-filters-ratings
v-if="showRatingFilter"
@rating-filter-selection="handleRatingFilterSelection"
/>
<div v-if="showBusinessHoursSwitch" class="business-hours">
<span class="business-hours-text ">
{{ $t('REPORT.BUSINESS_HOURS') }}
</span>
<span>
@@ -77,36 +48,54 @@
</template>
<script>
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 startOfDay from 'date-fns/startOfDay';
import getUnixTime from 'date-fns/getUnixTime';
import { GROUP_BY_FILTER } from '../constants';
import endOfDay from 'date-fns/endOfDay';
const CUSTOM_DATE_RANGE_ID = 5;
import { DATE_RANGE_OPTIONS } from '../constants';
import { getUnixStartOfDay, getUnixEndOfDay } from 'helpers/DateHelper';
export default {
components: {
WootDateRangePicker,
ReportsFiltersDateRange,
ReportsFiltersDateGroupBy,
ReportsFiltersAgents,
ReportsFiltersLabels,
ReportsFiltersInboxes,
ReportsFiltersTeams,
ReportsFiltersRatings,
},
props: {
filterItemsList: {
type: Array,
default: () => [],
},
agentsFilterItemsList: {
type: Array,
default: () => [],
},
selectedGroupByFilter: {
type: Object,
default: () => {},
},
groupByFilter: {
showGroupByFilter: {
type: Boolean,
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,
default: false,
},
@@ -117,95 +106,134 @@ export default {
},
data() {
return {
currentDateRangeSelection: this.$t('REPORT.DATE_RANGE')[0],
dateRange: this.$t('REPORT.DATE_RANGE'),
customDateRange: [new Date(), new Date()],
currentSelectedFilter: null,
// default value, need not be translated
selectedDateRange: DATE_RANGE_OPTIONS.LAST_7_DAYS,
selectedGroupByFilter: null,
selectedLabel: null,
selectedInbox: null,
selectedTeam: null,
selectedRating: null,
selectedAgents: [],
customDateRange: [new Date(), new Date()],
businessHoursSelected: false,
};
},
computed: {
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() {
if (this.isDateRangeSelected) {
return this.toCustomDate(this.customDateRange[1]);
return getUnixEndOfDay(this.customDateRange[1]);
}
return this.toCustomDate(new Date());
return getUnixEndOfDay(new Date());
},
from() {
if (this.isDateRangeSelected) {
return this.fromCustomDate(this.customDateRange[0]);
return getUnixStartOfDay(this.customDateRange[0]);
}
const dateRange = {
0: 6,
1: 29,
2: 89,
3: 179,
4: 364,
};
const diff = dateRange[this.currentDateRangeSelection.id];
const fromDate = subDays(new Date(), diff);
return this.fromCustomDate(fromDate);
const { offset } = this.selectedDateRange;
const fromDate = subDays(new Date(), offset);
return getUnixStartOfDay(fromDate);
},
groupBy() {
if (this.isDateRangeSelected) {
return GROUP_BY_FILTER[4].period;
validGroupOptions() {
return this.selectedDateRange.groupByOptions;
},
validGroupBy() {
if (!this.selectedGroupByFilter) {
return this.validGroupOptions[0];
}
const groupRange = {
0: GROUP_BY_FILTER[1].period,
1: GROUP_BY_FILTER[2].period,
2: GROUP_BY_FILTER[3].period,
3: GROUP_BY_FILTER[3].period,
4: GROUP_BY_FILTER[3].period,
};
return groupRange[this.currentDateRangeSelection.id];
},
notLast7Days() {
return this.groupBy !== GROUP_BY_FILTER[1].period;
const validIds = this.validGroupOptions.map(opt => opt.id);
if (validIds.includes(this.selectedGroupByFilter.id)) {
return this.selectedGroupByFilter;
}
return this.validGroupOptions[0];
},
},
watch: {
filterItemsList() {
this.currentSelectedFilter = this.selectedGroupByFilter;
},
businessHoursSelected() {
this.$emit('business-hours-toggle', this.businessHoursSelected);
this.emitChange();
},
},
mounted() {
this.onDateRangeChange();
this.emitChange();
},
methods: {
onDateRangeChange() {
this.$emit('date-range-change', {
from: this.from,
to: this.to,
groupBy: this.groupBy,
emitChange() {
const {
from,
to,
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) {
return getUnixTime(startOfDay(date));
onDateRangeChange(selectedRange) {
this.selectedDateRange = selectedRange;
this.selectedGroupByFilter = this.validGroupBy;
this.emitChange();
},
toCustomDate(date) {
return getUnixTime(endOfDay(date));
},
changeDateSelection(selectedRange) {
this.currentDateRangeSelection = selectedRange;
this.onDateRangeChange();
},
onChange(value) {
onCustomDateRangeChange(value) {
this.customDateRange = value;
this.onDateRangeChange();
this.selectedGroupByFilter = this.validGroupBy;
this.emitChange();
},
changeFilterSelection() {
this.$emit('filter-change', this.currentSelectedFilter);
onGroupingChange(payload) {
this.selectedGroupByFilter = payload;
this.emitChange();
},
handleAgentsFilterSelection() {
this.$emit('agents-filter-change', this.selectedAgents);
handleAgentsFilterSelection(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>
<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' },
};
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 =
'-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 = {
get: async function getResponses(
{ commit },
{ page = 1, from, to, user_ids } = {}
) {
get: async function getResponses({ commit }, params) {
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: true });
try {
const response = await CSATReports.get({ page, from, to, user_ids });
const response = await CSATReports.get(params);
commit(types.SET_CSAT_RESPONSE, response.data);
} catch (error) {
// Ignore error
@@ -99,10 +96,10 @@ export const actions = {
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 });
try {
const response = await CSATReports.getMetrics({ from, to, user_ids });
const response = await CSATReports.getMetrics(params);
commit(types.SET_CSAT_RESPONSE_METRICS, response.data);
} catch (error) {
// Ignore error

View File

@@ -59,24 +59,28 @@ export const ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP =
export const CSAT_RATINGS = [
{
key: 'disappointed',
translationKey: 'CSAT.RATINGS.POOR',
emoji: '😞',
value: 1,
color: '#FDAD2A',
},
{
key: 'expressionless',
translationKey: 'CSAT.RATINGS.FAIR',
emoji: '😑',
value: 2,
color: '#FFC532',
},
{
key: 'neutral',
translationKey: 'CSAT.RATINGS.AVERAGE',
emoji: '😐',
value: 3,
color: '#FCEC56',
},
{
key: 'grinning',
translationKey: 'CSAT.RATINGS.GOOD',
emoji: '😀',
value: 4,
color: '#6FD86F',
@@ -84,6 +88,7 @@ export const CSAT_RATINGS = [
{
key: 'smiling',
emoji: '😍',
translationKey: 'CSAT.RATINGS.EXCELLENT',
value: 5,
color: '#44CE4B',
},

View File

@@ -2,6 +2,7 @@ import fromUnixTime from 'date-fns/fromUnixTime';
import format from 'date-fns/format';
import isToday from 'date-fns/isToday';
import isYesterday from 'date-fns/isYesterday';
import { endOfDay, getUnixTime, startOfDay } from 'date-fns';
export const formatUnixDate = (date, dateFormat = 'MMM dd, yyyy') => {
const unixDate = fromUnixTime(date);
@@ -31,6 +32,12 @@ export const isTimeAfter = (h1, m1, h2, m2) => {
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) => {
const rtf = new Intl.RelativeTimeFormat(languageCode, {
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_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