mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
feat - Add filter for reports by agent, label and inboxes (#3084)
* Adds filter for agents, labels and inboxes * Added Inboxes Reports Feature * Fixed populating of filter dropdown issue * If applied, fixes code climate style-lint warnings * Fixes codeclimate warnings * if applied, Refactors sidebar file to fix codclimate warnings * if applied, fixes the download reports button for filtered report-data * If applied, replaces native img tag with thumbnail component * If applied, replaces hardcoded color string with variable * If applied, adds a11y labels to multiselect dropdowns * If applied, Renames reports methods to generic names * If applied, Adds test cases for Labels and Inboxes * If applied, write a test spec for fileDownload helper * if applied, Moves fileDownload method to a utils folder * If applied, Fixes the report file name type * Test Spec for Reports Store module * Fix specs - add restoreAllMocks Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
@@ -6,15 +6,15 @@ class ReportsAPI extends ApiClient {
|
||||
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
||||
}
|
||||
|
||||
getAccountReports(metric, since, until) {
|
||||
getReports(metric, since, until, type = 'account', id) {
|
||||
return axios.get(`${this.url}`, {
|
||||
params: { metric, since, until, type: 'account' },
|
||||
params: { metric, since, until, type, id },
|
||||
});
|
||||
}
|
||||
|
||||
getAccountSummary(since, until) {
|
||||
getSummary(since, until, type = 'account', id) {
|
||||
return axios.get(`${this.url}/summary`, {
|
||||
params: { since, until, type: 'account' },
|
||||
params: { since, until, type, id },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,18 @@ class ReportsAPI extends ApiClient {
|
||||
params: { since, until },
|
||||
});
|
||||
}
|
||||
|
||||
getLabelReports(since, until) {
|
||||
return axios.get(`${this.url}/labels`, {
|
||||
params: { since, until },
|
||||
});
|
||||
}
|
||||
|
||||
getInboxReports(since, until) {
|
||||
return axios.get(`${this.url}/inboxes`, {
|
||||
params: { since, until },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new ReportsAPI();
|
||||
|
||||
@@ -11,39 +11,34 @@ describe('#Reports API', () => {
|
||||
expect(reportsAPI).toHaveProperty('create');
|
||||
expect(reportsAPI).toHaveProperty('update');
|
||||
expect(reportsAPI).toHaveProperty('delete');
|
||||
expect(reportsAPI).toHaveProperty('getAccountReports');
|
||||
expect(reportsAPI).toHaveProperty('getAccountSummary');
|
||||
expect(reportsAPI).toHaveProperty('getReports');
|
||||
expect(reportsAPI).toHaveProperty('getSummary');
|
||||
expect(reportsAPI).toHaveProperty('getAgentReports');
|
||||
expect(reportsAPI).toHaveProperty('getLabelReports');
|
||||
expect(reportsAPI).toHaveProperty('getInboxReports');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#getAccountReports', () => {
|
||||
reportsAPI.getAccountReports(
|
||||
'conversations_count',
|
||||
1621103400,
|
||||
1621621800
|
||||
);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports',
|
||||
{
|
||||
params: {
|
||||
metric: 'conversations_count',
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
type: 'account'
|
||||
},
|
||||
}
|
||||
);
|
||||
reportsAPI.getReports('conversations_count', 1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
|
||||
params: {
|
||||
metric: 'conversations_count',
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
type: 'account',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#getAccountSummary', () => {
|
||||
reportsAPI.getAccountSummary(1621103400, 1621621800);
|
||||
reportsAPI.getSummary(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/summary',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
type: 'account'
|
||||
type: 'account',
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -61,5 +56,31 @@ describe('#Reports API', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#getLabelReports', () => {
|
||||
reportsAPI.getLabelReports(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/labels',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#getInboxReports', () => {
|
||||
reportsAPI.getInboxReports(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/inboxes',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.report-bar {
|
||||
@include margin(-1px $zero);
|
||||
@include background-white;
|
||||
|
||||
30
app/javascript/dashboard/assets/scss/widgets/_reports.scss
Normal file
30
app/javascript/dashboard/assets/scss/widgets/_reports.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
.date-picker {
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
|
||||
.margin-left-small {
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
|
||||
.reports-option__rounded--item {
|
||||
border-radius: 100%;
|
||||
height: var(--space-two);
|
||||
width: var(--space-two);
|
||||
}
|
||||
|
||||
.reports-option__item {
|
||||
flex-shrink: 0;
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.reports-option__label--swatch {
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.margin-right-small {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.display-flex {
|
||||
display: flex;
|
||||
}
|
||||
@@ -1,245 +1,13 @@
|
||||
import { frontendURL } from '../helper/URLHelper';
|
||||
import common from './sidebarItems/common';
|
||||
import contacts from './sidebarItems/contacts';
|
||||
import reports from './sidebarItems/reports';
|
||||
import campaigns from './sidebarItems/campaigns';
|
||||
import settings from './sidebarItems/settings';
|
||||
|
||||
export const getSidebarItems = accountId => ({
|
||||
common: {
|
||||
routes: [
|
||||
'home',
|
||||
'inbox_dashboard',
|
||||
'inbox_conversation',
|
||||
'conversation_through_inbox',
|
||||
'notifications_dashboard',
|
||||
'profile_settings',
|
||||
'profile_settings_index',
|
||||
'label_conversations',
|
||||
'conversations_through_label',
|
||||
'team_conversations',
|
||||
'conversations_through_team',
|
||||
'notifications_index',
|
||||
],
|
||||
menuItems: {
|
||||
assignedToMe: {
|
||||
icon: 'ion-chatbox-working',
|
||||
label: 'CONVERSATIONS',
|
||||
hasSubMenu: false,
|
||||
key: '',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
toolTip: 'Conversation from all subscribed inboxes',
|
||||
toStateName: 'home',
|
||||
},
|
||||
contacts: {
|
||||
icon: 'ion-person',
|
||||
label: 'CONTACTS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||
toStateName: 'contacts_dashboard',
|
||||
},
|
||||
notifications: {
|
||||
icon: 'ion-ios-bell',
|
||||
label: 'NOTIFICATIONS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/notifications`),
|
||||
toStateName: 'notifications_dashboard',
|
||||
},
|
||||
report: {
|
||||
icon: 'ion-arrow-graph-up-right',
|
||||
label: 'REPORTS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports`),
|
||||
toStateName: 'settings_account_reports',
|
||||
},
|
||||
campaigns: {
|
||||
icon: 'ion-speakerphone',
|
||||
label: 'CAMPAIGNS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
||||
toStateName: 'settings_account_campaigns',
|
||||
},
|
||||
settings: {
|
||||
icon: 'ion-settings',
|
||||
label: 'SETTINGS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings`),
|
||||
toStateName: 'settings_home',
|
||||
},
|
||||
},
|
||||
},
|
||||
contacts: {
|
||||
routes: [
|
||||
'contacts_dashboard',
|
||||
'contacts_dashboard_manage',
|
||||
'contacts_labels_dashboard',
|
||||
],
|
||||
menuItems: {
|
||||
back: {
|
||||
icon: 'ion-ios-arrow-back',
|
||||
label: 'HOME',
|
||||
hasSubMenu: false,
|
||||
toStateName: 'home',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
},
|
||||
contacts: {
|
||||
icon: 'ion-person',
|
||||
label: 'ALL_CONTACTS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||
toStateName: 'contacts_dashboard',
|
||||
},
|
||||
},
|
||||
},
|
||||
reports: {
|
||||
routes: ['settings_account_reports', 'csat_reports'],
|
||||
menuItems: {
|
||||
back: {
|
||||
icon: 'ion-ios-arrow-back',
|
||||
label: 'HOME',
|
||||
hasSubMenu: false,
|
||||
toStateName: 'home',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
},
|
||||
reportOverview: {
|
||||
icon: 'ion-arrow-graph-up-right',
|
||||
label: 'REPORTS_OVERVIEW',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/overview`),
|
||||
toStateName: 'settings_account_reports',
|
||||
},
|
||||
csatReports: {
|
||||
icon: 'ion-happy',
|
||||
label: 'CSAT',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/csat`),
|
||||
toStateName: 'csat_reports',
|
||||
},
|
||||
},
|
||||
},
|
||||
campaigns: {
|
||||
routes: ['settings_account_campaigns', 'one_off'],
|
||||
menuItems: {
|
||||
back: {
|
||||
icon: 'ion-ios-arrow-back',
|
||||
label: 'HOME',
|
||||
hasSubMenu: false,
|
||||
toStateName: 'home',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
},
|
||||
ongoingCampaigns: {
|
||||
icon: 'ion-arrow-swap',
|
||||
label: 'ONGOING',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns/ongoing`),
|
||||
toStateName: 'settings_account_campaigns',
|
||||
},
|
||||
onOffCampaigns: {
|
||||
icon: 'ion-radio-waves',
|
||||
label: 'ONE_OFF',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns/one_off`),
|
||||
toStateName: 'one_off',
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
routes: [
|
||||
'agent_list',
|
||||
'canned_list',
|
||||
'labels_list',
|
||||
'settings_inbox',
|
||||
'attributes_list',
|
||||
'settings_inbox_new',
|
||||
'settings_inbox_list',
|
||||
'settings_inbox_show',
|
||||
'settings_inboxes_page_channel',
|
||||
'settings_inboxes_add_agents',
|
||||
'settings_inbox_finish',
|
||||
'settings_integrations',
|
||||
'settings_integrations_webhook',
|
||||
'settings_integrations_integration',
|
||||
'settings_applications',
|
||||
'settings_applications_webhook',
|
||||
'settings_applications_integration',
|
||||
'general_settings',
|
||||
'general_settings_index',
|
||||
'settings_teams_list',
|
||||
'settings_teams_new',
|
||||
'settings_teams_add_agents',
|
||||
'settings_teams_finish',
|
||||
'settings_teams_edit',
|
||||
'settings_teams_edit_members',
|
||||
'settings_teams_edit_finish',
|
||||
],
|
||||
menuItems: {
|
||||
back: {
|
||||
icon: 'ion-ios-arrow-back',
|
||||
label: 'HOME',
|
||||
hasSubMenu: false,
|
||||
toStateName: 'home',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
},
|
||||
agents: {
|
||||
icon: 'ion-person-stalker',
|
||||
label: 'AGENTS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
|
||||
toStateName: 'agent_list',
|
||||
},
|
||||
teams: {
|
||||
icon: 'ion-ios-people',
|
||||
label: 'TEAMS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
|
||||
toStateName: 'settings_teams_list',
|
||||
},
|
||||
inboxes: {
|
||||
icon: 'ion-archive',
|
||||
label: 'INBOXES',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
|
||||
toStateName: 'settings_inbox_list',
|
||||
},
|
||||
labels: {
|
||||
icon: 'ion-pricetags',
|
||||
label: 'LABELS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
|
||||
toStateName: 'labels_list',
|
||||
},
|
||||
attributes: {
|
||||
icon: 'ion-code',
|
||||
label: 'ATTRIBUTES',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/attributes/list`),
|
||||
toStateName: 'attributes_list',
|
||||
},
|
||||
cannedResponses: {
|
||||
icon: 'ion-chatbox-working',
|
||||
label: 'CANNED_RESPONSES',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(
|
||||
`accounts/${accountId}/settings/canned-response/list`
|
||||
),
|
||||
toStateName: 'canned_list',
|
||||
},
|
||||
settings_integrations: {
|
||||
icon: 'ion-flash',
|
||||
label: 'INTEGRATIONS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
||||
toStateName: 'settings_integrations',
|
||||
},
|
||||
settings_applications: {
|
||||
icon: 'ion-asterisk',
|
||||
label: 'APPLICATIONS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
||||
toStateName: 'settings_applications',
|
||||
},
|
||||
general_settings_index: {
|
||||
icon: 'ion-gear-a',
|
||||
label: 'ACCOUNT_SETTINGS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/general`),
|
||||
toStateName: 'general_settings_index',
|
||||
},
|
||||
},
|
||||
},
|
||||
common: common(accountId),
|
||||
contacts: contacts(accountId),
|
||||
reports: reports(accountId),
|
||||
campaigns: campaigns(accountId),
|
||||
settings: settings(accountId),
|
||||
});
|
||||
|
||||
@@ -61,6 +61,195 @@
|
||||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
"HEADER": "Agents Overview",
|
||||
"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",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Agent",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Conversations",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "Incoming Messages",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "Outgoing Messages",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "First response time",
|
||||
"DESC": "( Avg )"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
"DESC": "( Total )"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Last 7 days"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Last 30 days"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"LABEL_REPORTS": {
|
||||
"HEADER": "Labels Overview",
|
||||
"LOADING_CHART": "Loading chart data...",
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"DOWNLOAD_LABEL_REPORTS": "Download label reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Label",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Conversations",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "Incoming Messages",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "Outgoing Messages",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "First response time",
|
||||
"DESC": "( Avg )"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
"DESC": "( Total )"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Last 7 days"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Last 30 days"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"INBOX_REPORTS": {
|
||||
"HEADER": "Inbox Overview",
|
||||
"LOADING_CHART": "Loading chart data...",
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"DOWNLOAD_INBOX_REPORTS": "Download inbox reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Inbox",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Conversations",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "Incoming Messages",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "Outgoing Messages",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "First response time",
|
||||
"DESC": "( Avg )"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
"DESC": "( Total )"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Last 7 days"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Last 30 days"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"CSAT_REPORTS": {
|
||||
"HEADER": "CSAT Reports",
|
||||
"NO_RECORDS": "There are no CSAT survey responses available.",
|
||||
@@ -78,13 +267,13 @@
|
||||
"TOOLTIP": "Total number of responses collected"
|
||||
},
|
||||
"SATISFACTION_SCORE": {
|
||||
"LABEL": "Satisfaction score",
|
||||
"TOOLTIP": "Total number of positive responses / Total number of responses * 100"
|
||||
"LABEL": "Satisfaction score",
|
||||
"TOOLTIP": "Total number of positive responses / Total number of responses * 100"
|
||||
},
|
||||
"RESPONSE_RATE": {
|
||||
"LABEL": "Response rate",
|
||||
"TOOLTIP": "Total number of responses / Total number of CSAT survey messages sent * 100"
|
||||
"LABEL": "Response rate",
|
||||
"TOOLTIP": "Total number of responses / Total number of CSAT survey messages sent * 100"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,10 @@
|
||||
"CSAT": "CSAT",
|
||||
"CAMPAIGNS": "Campaigns",
|
||||
"ONGOING": "Ongoing",
|
||||
"ONE_OFF": "One off"
|
||||
"ONE_OFF": "One off",
|
||||
"REPORTS_AGENT": "Agents",
|
||||
"REPORTS_LABEL": "Labels",
|
||||
"REPORTS_INBOX": "Inbox"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
|
||||
|
||||
30
app/javascript/dashboard/i18n/sidebarItems/campaigns.js
Normal file
30
app/javascript/dashboard/i18n/sidebarItems/campaigns.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
|
||||
const campaigns = accountId => ({
|
||||
routes: ['settings_account_campaigns', 'one_off'],
|
||||
menuItems: {
|
||||
back: {
|
||||
icon: 'ion-ios-arrow-back',
|
||||
label: 'HOME',
|
||||
hasSubMenu: false,
|
||||
toStateName: 'home',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
},
|
||||
ongoingCampaigns: {
|
||||
icon: 'ion-arrow-swap',
|
||||
label: 'ONGOING',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns/ongoing`),
|
||||
toStateName: 'settings_account_campaigns',
|
||||
},
|
||||
onOffCampaigns: {
|
||||
icon: 'ion-radio-waves',
|
||||
label: 'ONE_OFF',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns/one_off`),
|
||||
toStateName: 'one_off',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default campaigns;
|
||||
66
app/javascript/dashboard/i18n/sidebarItems/common.js
Normal file
66
app/javascript/dashboard/i18n/sidebarItems/common.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
|
||||
const common = accountId => ({
|
||||
routes: [
|
||||
'home',
|
||||
'inbox_dashboard',
|
||||
'inbox_conversation',
|
||||
'conversation_through_inbox',
|
||||
'notifications_dashboard',
|
||||
'profile_settings',
|
||||
'profile_settings_index',
|
||||
'label_conversations',
|
||||
'conversations_through_label',
|
||||
'team_conversations',
|
||||
'conversations_through_team',
|
||||
'notifications_index',
|
||||
],
|
||||
menuItems: {
|
||||
assignedToMe: {
|
||||
icon: 'ion-chatbox-working',
|
||||
label: 'CONVERSATIONS',
|
||||
hasSubMenu: false,
|
||||
key: '',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
toolTip: 'Conversation from all subscribed inboxes',
|
||||
toStateName: 'home',
|
||||
},
|
||||
contacts: {
|
||||
icon: 'ion-person',
|
||||
label: 'CONTACTS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||
toStateName: 'contacts_dashboard',
|
||||
},
|
||||
notifications: {
|
||||
icon: 'ion-ios-bell',
|
||||
label: 'NOTIFICATIONS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/notifications`),
|
||||
toStateName: 'notifications_dashboard',
|
||||
},
|
||||
report: {
|
||||
icon: 'ion-arrow-graph-up-right',
|
||||
label: 'REPORTS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports`),
|
||||
toStateName: 'settings_account_reports',
|
||||
},
|
||||
campaigns: {
|
||||
icon: 'ion-speakerphone',
|
||||
label: 'CAMPAIGNS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
||||
toStateName: 'settings_account_campaigns',
|
||||
},
|
||||
settings: {
|
||||
icon: 'ion-settings',
|
||||
label: 'SETTINGS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings`),
|
||||
toStateName: 'settings_home',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default common;
|
||||
27
app/javascript/dashboard/i18n/sidebarItems/contacts.js
Normal file
27
app/javascript/dashboard/i18n/sidebarItems/contacts.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
|
||||
const contacts = accountId => ({
|
||||
routes: [
|
||||
'contacts_dashboard',
|
||||
'contacts_dashboard_manage',
|
||||
'contacts_labels_dashboard',
|
||||
],
|
||||
menuItems: {
|
||||
back: {
|
||||
icon: 'ion-ios-arrow-back',
|
||||
label: 'HOME',
|
||||
hasSubMenu: false,
|
||||
toStateName: 'home',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
},
|
||||
contacts: {
|
||||
icon: 'ion-person',
|
||||
label: 'ALL_CONTACTS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||
toStateName: 'contacts_dashboard',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default contacts;
|
||||
57
app/javascript/dashboard/i18n/sidebarItems/reports.js
Normal file
57
app/javascript/dashboard/i18n/sidebarItems/reports.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
|
||||
const reports = accountId => ({
|
||||
routes: [
|
||||
'settings_account_reports',
|
||||
'csat_reports',
|
||||
'agent_reports',
|
||||
'label_reports',
|
||||
'inbox_reports',
|
||||
],
|
||||
menuItems: {
|
||||
back: {
|
||||
icon: 'ion-ios-arrow-back',
|
||||
label: 'HOME',
|
||||
hasSubMenu: false,
|
||||
toStateName: 'home',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
},
|
||||
reportOverview: {
|
||||
icon: 'ion-arrow-graph-up-right',
|
||||
label: 'REPORTS_OVERVIEW',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/overview`),
|
||||
toStateName: 'settings_account_reports',
|
||||
},
|
||||
csatReports: {
|
||||
icon: 'ion-happy',
|
||||
label: 'CSAT',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/csat`),
|
||||
toStateName: 'csat_reports',
|
||||
},
|
||||
agentReports: {
|
||||
icon: 'ion-ios-people',
|
||||
label: 'REPORTS_AGENT',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/agent`),
|
||||
toStateName: 'agent_reports',
|
||||
},
|
||||
labelReports: {
|
||||
icon: 'ion-pricetags',
|
||||
label: 'REPORTS_LABEL',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/label`),
|
||||
toStateName: 'label_reports',
|
||||
},
|
||||
inboxReports: {
|
||||
icon: 'ion-archive',
|
||||
label: 'REPORTS_INBOX',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/inboxes`),
|
||||
toStateName: 'inbox_reports',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default reports;
|
||||
108
app/javascript/dashboard/i18n/sidebarItems/settings.js
Normal file
108
app/javascript/dashboard/i18n/sidebarItems/settings.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
|
||||
const settings = accountId => ({
|
||||
routes: [
|
||||
'agent_list',
|
||||
'canned_list',
|
||||
'labels_list',
|
||||
'settings_inbox',
|
||||
'attributes_list',
|
||||
'settings_inbox_new',
|
||||
'settings_inbox_list',
|
||||
'settings_inbox_show',
|
||||
'settings_inboxes_page_channel',
|
||||
'settings_inboxes_add_agents',
|
||||
'settings_inbox_finish',
|
||||
'settings_integrations',
|
||||
'settings_integrations_webhook',
|
||||
'settings_integrations_integration',
|
||||
'settings_applications',
|
||||
'settings_applications_webhook',
|
||||
'settings_applications_integration',
|
||||
'general_settings',
|
||||
'general_settings_index',
|
||||
'settings_teams_list',
|
||||
'settings_teams_new',
|
||||
'settings_teams_add_agents',
|
||||
'settings_teams_finish',
|
||||
'settings_teams_edit',
|
||||
'settings_teams_edit_members',
|
||||
'settings_teams_edit_finish',
|
||||
],
|
||||
menuItems: {
|
||||
back: {
|
||||
icon: 'ion-ios-arrow-back',
|
||||
label: 'HOME',
|
||||
hasSubMenu: false,
|
||||
toStateName: 'home',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
},
|
||||
agents: {
|
||||
icon: 'ion-person-stalker',
|
||||
label: 'AGENTS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
|
||||
toStateName: 'agent_list',
|
||||
},
|
||||
teams: {
|
||||
icon: 'ion-ios-people',
|
||||
label: 'TEAMS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
|
||||
toStateName: 'settings_teams_list',
|
||||
},
|
||||
inboxes: {
|
||||
icon: 'ion-archive',
|
||||
label: 'INBOXES',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
|
||||
toStateName: 'settings_inbox_list',
|
||||
},
|
||||
labels: {
|
||||
icon: 'ion-pricetags',
|
||||
label: 'LABELS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
|
||||
toStateName: 'labels_list',
|
||||
},
|
||||
attributes: {
|
||||
icon: 'ion-code',
|
||||
label: 'ATTRIBUTES',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/attributes/list`),
|
||||
toStateName: 'attributes_list',
|
||||
},
|
||||
cannedResponses: {
|
||||
icon: 'ion-chatbox-working',
|
||||
label: 'CANNED_RESPONSES',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(
|
||||
`accounts/${accountId}/settings/canned-response/list`
|
||||
),
|
||||
toStateName: 'canned_list',
|
||||
},
|
||||
settings_integrations: {
|
||||
icon: 'ion-flash',
|
||||
label: 'INTEGRATIONS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
||||
toStateName: 'settings_integrations',
|
||||
},
|
||||
settings_applications: {
|
||||
icon: 'ion-asterisk',
|
||||
label: 'APPLICATIONS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
||||
toStateName: 'settings_applications',
|
||||
},
|
||||
general_settings_index: {
|
||||
icon: 'ion-gear-a',
|
||||
label: 'ACCOUNT_SETTINGS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/general`),
|
||||
toStateName: 'general_settings_index',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default settings;
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="row app-wrapper">
|
||||
<sidebar :route="currentRoute" :class="sidebarClassName"></sidebar>
|
||||
<section class="app-content columns" :class="contentClassName">
|
||||
<router-view></router-view>
|
||||
<router-view :key="$route.path"></router-view>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<woot-reports
|
||||
key="agent-reports"
|
||||
type="agent"
|
||||
getter-key="agents/getAgents"
|
||||
action-key="agents/get"
|
||||
:download-button-label="$t('REPORT.DOWNLOAD_AGENT_REPORTS')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WootReports from './components/WootReports';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootReports,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<woot-reports
|
||||
key="inbox-reports"
|
||||
type="inbox"
|
||||
getter-key="inboxes/getInboxes"
|
||||
action-key="inboxes/get"
|
||||
:download-button-label="$t('INBOX_REPORTS.DOWNLOAD_INBOX_REPORTS')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WootReports from './components/WootReports';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootReports,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<woot-reports
|
||||
key="label-reports"
|
||||
type="label"
|
||||
getter-key="labels/getLabels"
|
||||
action-key="labels/get"
|
||||
:download-button-label="$t('LABEL_REPORTS.DOWNLOAD_LABEL_REPORTS')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WootReports from './components/WootReports';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootReports,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="flex-container flex-dir-column medium-flex-dir-row">
|
||||
<div v-if="type === 'agent'" class="small-12 medium-3 pull-right">
|
||||
<p aria-hidden="true" class="hide">
|
||||
{{ $t('AGENT_REPORTS.FILTER_DROPDOWN_LABEL') }}
|
||||
</p>
|
||||
<multiselect
|
||||
v-model="currentSelectedFilter"
|
||||
:placeholder="$t('AGENT_REPORTS.FILTER_DROPDOWN_LABEL')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
:options="filterItemsList"
|
||||
:option-height="24"
|
||||
:show-labels="false"
|
||||
@input="changeFilterSelection"
|
||||
>
|
||||
<template slot="singleLabel" slot-scope="props">
|
||||
<div class="display-flex">
|
||||
<thumbnail
|
||||
src="props.option.thumbnail"
|
||||
:username="props.option.name"
|
||||
size="22px"
|
||||
class="margin-right-small"
|
||||
/>
|
||||
<span class="reports-option__desc">
|
||||
<span class="reports-option__title">{{ props.option.name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="option" slot-scope="props">
|
||||
<div class="display-flex">
|
||||
<thumbnail
|
||||
src="props.option.thumbnail"
|
||||
:username="props.option.name"
|
||||
size="22px"
|
||||
class="margin-right-small"
|
||||
/>
|
||||
<p>{{ props.option.name }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</multiselect>
|
||||
</div>
|
||||
<div v-if="type === 'label'" class="small-12 medium-3 pull-right">
|
||||
<p aria-hidden="true" class="hide">
|
||||
{{ $t('LABEL_REPORTS.FILTER_DROPDOWN_LABEL') }}
|
||||
</p>
|
||||
<multiselect
|
||||
v-model="currentSelectedFilter"
|
||||
:placeholder="$t('LABEL_REPORTS.FILTER_DROPDOWN_LABEL')"
|
||||
label="title"
|
||||
track-by="id"
|
||||
:options="filterItemsList"
|
||||
:option-height="24"
|
||||
:show-labels="false"
|
||||
@input="changeFilterSelection"
|
||||
>
|
||||
<template slot="singleLabel" slot-scope="props">
|
||||
<div class="display-flex">
|
||||
<div
|
||||
:style="{ backgroundColor: props.option.color }"
|
||||
class="reports-option__rounded--item margin-right-small"
|
||||
></div>
|
||||
<span class="reports-option__desc">
|
||||
<span class="reports-option__title">{{
|
||||
props.option.title
|
||||
}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="option" slot-scope="props">
|
||||
<div class="display-flex">
|
||||
<div
|
||||
:style="{ backgroundColor: props.option.color }"
|
||||
class="
|
||||
reports-option__rounded--item
|
||||
reports-option__item
|
||||
reports-option__label--swatch
|
||||
"
|
||||
></div>
|
||||
<span class="reports-option__desc">
|
||||
<span class="reports-option__title">{{
|
||||
props.option.title
|
||||
}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</multiselect>
|
||||
</div>
|
||||
<div v-if="type === 'inbox'" class="small-12 medium-3 pull-right">
|
||||
<p aria-hidden="true" class="hide">
|
||||
{{ $t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL') }}
|
||||
</p>
|
||||
<multiselect
|
||||
v-model="currentSelectedFilter"
|
||||
track-by="id"
|
||||
label="name"
|
||||
:placeholder="$t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL')"
|
||||
selected-label
|
||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||
deselect-label=""
|
||||
:options="filterItemsList"
|
||||
:searchable="false"
|
||||
:allow-empty="false"
|
||||
@input="changeFilterSelection"
|
||||
/>
|
||||
</div>
|
||||
<div class="small-12 medium-3 pull-right margin-left-small">
|
||||
<multiselect
|
||||
v-model="currentDateRangeSelection"
|
||||
track-by="name"
|
||||
label="name"
|
||||
:placeholder="$t('FORMS.MULTISELECT.SELECT_ONE')"
|
||||
selected-label
|
||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||
deselect-label=""
|
||||
:options="dateRange"
|
||||
:searchable="false"
|
||||
:allow-empty="false"
|
||||
@select="changeDateSelection"
|
||||
/>
|
||||
</div>
|
||||
<woot-date-range-picker
|
||||
v-if="isDateRangeSelected"
|
||||
show-range
|
||||
:value="customDateRange"
|
||||
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
|
||||
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
||||
const CUSTOM_DATE_RANGE_ID = 5;
|
||||
import subDays from 'date-fns/subDays';
|
||||
import startOfDay from 'date-fns/startOfDay';
|
||||
import getUnixTime from 'date-fns/getUnixTime';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDateRangePicker,
|
||||
Thumbnail,
|
||||
},
|
||||
props: {
|
||||
filterItemsList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'agent',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentSelectedFilter: null,
|
||||
currentDateRangeSelection: this.$t('REPORT.DATE_RANGE')[0],
|
||||
dateRange: this.$t('REPORT.DATE_RANGE'),
|
||||
customDateRange: [new Date(), new Date()],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDateRangeSelected() {
|
||||
return this.currentDateRangeSelection.id === CUSTOM_DATE_RANGE_ID;
|
||||
},
|
||||
to() {
|
||||
if (this.isDateRangeSelected) {
|
||||
return this.fromCustomDate(this.customDateRange[1]);
|
||||
}
|
||||
return this.fromCustomDate(new Date());
|
||||
},
|
||||
from() {
|
||||
if (this.isDateRangeSelected) {
|
||||
return this.fromCustomDate(this.customDateRange[0]);
|
||||
}
|
||||
const dateRange = {
|
||||
0: 6,
|
||||
1: 29,
|
||||
2: 89,
|
||||
3: 179,
|
||||
4: 364,
|
||||
};
|
||||
const diff = dateRange[this.currentDateRangeSelection.id];
|
||||
const fromDate = subDays(new Date(), diff);
|
||||
return this.fromCustomDate(fromDate);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
filterItemsList(val) {
|
||||
this.currentSelectedFilter = val[0];
|
||||
},
|
||||
currentSelectedFilter() {
|
||||
this.changeFilterSelection();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.onDateRangeChange();
|
||||
},
|
||||
methods: {
|
||||
onDateRangeChange() {
|
||||
this.$emit('date-range-change', { from: this.from, to: this.to });
|
||||
},
|
||||
fromCustomDate(date) {
|
||||
return getUnixTime(startOfDay(date));
|
||||
},
|
||||
changeDateSelection(selectedRange) {
|
||||
this.currentDateRangeSelection = selectedRange;
|
||||
this.onDateRangeChange();
|
||||
},
|
||||
changeFilterSelection() {
|
||||
this.$emit('filter-change', this.currentSelectedFilter);
|
||||
},
|
||||
onChange(value) {
|
||||
this.customDateRange = value;
|
||||
this.onDateRangeChange();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~dashboard/assets/scss/widgets/_reports';
|
||||
</style>
|
||||
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="column content-box">
|
||||
<woot-button
|
||||
color-scheme="success"
|
||||
class-names="button--fixed-right-top"
|
||||
icon="ion-android-download"
|
||||
@click="downloadReports"
|
||||
>
|
||||
{{ downloadButtonLabel }}
|
||||
</woot-button>
|
||||
<report-filters
|
||||
v-if="filterItemsList"
|
||||
:type="type"
|
||||
:filter-items-list="filterItemsList"
|
||||
@date-range-change="onDateRangeChange"
|
||||
@filter-change="onFilterChange"
|
||||
/>
|
||||
<div v-if="selectedFilter">
|
||||
<div class="row">
|
||||
<woot-report-stats-card
|
||||
v-for="(metric, index) in metrics"
|
||||
:key="metric.NAME"
|
||||
:desc="metric.DESC"
|
||||
:heading="metric.NAME"
|
||||
:index="index"
|
||||
:on-click="changeSelection"
|
||||
:point="accountSummary[metric.KEY]"
|
||||
:selected="index === currentSelection"
|
||||
/>
|
||||
</div>
|
||||
<div class="report-bar">
|
||||
<woot-loading-state
|
||||
v-if="accountReport.isFetching"
|
||||
:message="$t('REPORT.LOADING_CHART')"
|
||||
/>
|
||||
<div v-else class="chart-container">
|
||||
<woot-bar v-if="accountReport.data.length" :collection="collection" />
|
||||
<span v-else class="empty-state">
|
||||
{{ $t('REPORT.NO_ENOUGH_DATA') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ReportFilters from './ReportFilters';
|
||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
import format from 'date-fns/format';
|
||||
|
||||
const REPORTS_KEYS = {
|
||||
CONVERSATIONS: 'conversations_count',
|
||||
INCOMING_MESSAGES: 'incoming_messages_count',
|
||||
OUTGOING_MESSAGES: 'outgoing_messages_count',
|
||||
FIRST_RESPONSE_TIME: 'avg_first_response_time',
|
||||
RESOLUTION_TIME: 'avg_resolution_time',
|
||||
RESOLUTION_COUNT: 'resolutions_count',
|
||||
};
|
||||
export default {
|
||||
components: {
|
||||
ReportFilters,
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'account',
|
||||
},
|
||||
getterKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
actionKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
downloadButtonLabel: {
|
||||
type: String,
|
||||
default: 'Download Reports',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
from: 0,
|
||||
to: 0,
|
||||
currentSelection: 0,
|
||||
selectedFilter: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filterItemsList() {
|
||||
return this.$store.getters[this.getterKey] || [];
|
||||
},
|
||||
accountSummary() {
|
||||
return this.$store.getters.getAccountSummary || [];
|
||||
},
|
||||
accountReport() {
|
||||
return this.$store.getters.getAccountReports || [];
|
||||
},
|
||||
collection() {
|
||||
if (this.accountReport.isFetching) {
|
||||
return {};
|
||||
}
|
||||
if (!this.accountReport.data.length) return {};
|
||||
const labels = this.accountReport.data.map(element =>
|
||||
format(fromUnixTime(element.timestamp), 'dd/MMM')
|
||||
);
|
||||
const data = this.accountReport.data.map(element => element.value);
|
||||
return {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: this.metrics[this.currentSelection].NAME,
|
||||
backgroundColor: '#1f93ff',
|
||||
data,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
metrics() {
|
||||
const reportKeys = [
|
||||
'CONVERSATIONS',
|
||||
'INCOMING_MESSAGES',
|
||||
'OUTGOING_MESSAGES',
|
||||
'FIRST_RESPONSE_TIME',
|
||||
'RESOLUTION_TIME',
|
||||
'RESOLUTION_COUNT',
|
||||
];
|
||||
return reportKeys.map(key => ({
|
||||
NAME: this.$t(`REPORT.METRICS.${key}.NAME`),
|
||||
KEY: REPORTS_KEYS[key],
|
||||
DESC: this.$t(`REPORT.METRICS.${key}.DESC`),
|
||||
}));
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch(this.actionKey);
|
||||
},
|
||||
methods: {
|
||||
fetchAllData() {
|
||||
if (this.selectedFilter) {
|
||||
const { from, to } = this;
|
||||
this.$store.dispatch('fetchAccountSummary', {
|
||||
from,
|
||||
to,
|
||||
type: this.type,
|
||||
id: this.selectedFilter.id,
|
||||
});
|
||||
this.fetchChartData();
|
||||
}
|
||||
},
|
||||
fetchChartData() {
|
||||
const { from, to } = this;
|
||||
this.$store.dispatch('fetchAccountReport', {
|
||||
metric: this.metrics[this.currentSelection].KEY,
|
||||
from,
|
||||
to,
|
||||
type: this.type,
|
||||
id: this.selectedFilter.id,
|
||||
});
|
||||
},
|
||||
downloadReports() {
|
||||
const { from, to } = this;
|
||||
const fileName = `${this.type}-report-${format(
|
||||
fromUnixTime(to),
|
||||
'dd-MM-yyyy'
|
||||
)}.csv`;
|
||||
switch (this.type) {
|
||||
case 'agent':
|
||||
this.$store.dispatch('downloadAgentReports', { from, to, fileName });
|
||||
break;
|
||||
case 'label':
|
||||
this.$store.dispatch('downloadLabelReports', { from, to, fileName });
|
||||
break;
|
||||
case 'inbox':
|
||||
this.$store.dispatch('downloadInboxReports', { from, to, fileName });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
changeSelection(index) {
|
||||
this.currentSelection = index;
|
||||
this.fetchChartData();
|
||||
},
|
||||
onDateRangeChange({ from, to }) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.fetchAllData();
|
||||
},
|
||||
onFilterChange(payload) {
|
||||
if (payload) {
|
||||
this.selectedFilter = payload;
|
||||
this.fetchAllData();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,4 +1,7 @@
|
||||
import Index from './Index';
|
||||
import AgentReports from './AgentReports';
|
||||
import LabelReports from './LabelReports';
|
||||
import InboxReports from './InboxReports';
|
||||
import CsatResponses from './CsatResponses';
|
||||
import SettingsContent from '../Wrapper';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
@@ -41,5 +44,53 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/reports'),
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'AGENT_REPORTS.HEADER',
|
||||
icon: 'ion-people',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'agent',
|
||||
name: 'agent_reports',
|
||||
roles: ['administrator'],
|
||||
component: AgentReports,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/reports'),
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'LABEL_REPORTS.HEADER',
|
||||
icon: 'ion-pricetags',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'label',
|
||||
name: 'label_reports',
|
||||
roles: ['administrator'],
|
||||
component: LabelReports,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/reports'),
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'INBOX_REPORTS.HEADER',
|
||||
icon: 'ion-archive',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'inboxes',
|
||||
name: 'inbox_reports',
|
||||
roles: ['administrator'],
|
||||
component: InboxReports,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -36,10 +36,12 @@ const getters = {
|
||||
export const actions = {
|
||||
fetchAccountReport({ commit }, reportObj) {
|
||||
commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, true);
|
||||
Report.getAccountReports(
|
||||
Report.getReports(
|
||||
reportObj.metric,
|
||||
reportObj.from,
|
||||
reportObj.to
|
||||
reportObj.to,
|
||||
reportObj.type,
|
||||
reportObj.id
|
||||
).then(accountReport => {
|
||||
let { data } = accountReport;
|
||||
data = data.filter(
|
||||
@@ -60,7 +62,12 @@ export const actions = {
|
||||
});
|
||||
},
|
||||
fetchAccountSummary({ commit }, reportObj) {
|
||||
Report.getAccountSummary(reportObj.from, reportObj.to)
|
||||
Report.getSummary(
|
||||
reportObj.from,
|
||||
reportObj.to,
|
||||
reportObj.type,
|
||||
reportObj.id
|
||||
)
|
||||
.then(accountSummary => {
|
||||
commit(types.default.SET_ACCOUNT_SUMMARY, accountSummary.data);
|
||||
})
|
||||
@@ -85,6 +92,40 @@ export const actions = {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
downloadLabelReports(_, reportObj) {
|
||||
return Report.getLabelReports(reportObj.from, reportObj.to)
|
||||
.then(response => {
|
||||
let csvContent = 'data:text/csv;charset=utf-8,' + response.data;
|
||||
var encodedUri = encodeURI(csvContent);
|
||||
var downloadLink = document.createElement('a');
|
||||
downloadLink.href = encodedUri;
|
||||
downloadLink.download = reportObj.fileName;
|
||||
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
downloadInboxReports(_, reportObj) {
|
||||
return Report.getInboxReports(reportObj.from, reportObj.to)
|
||||
.then(response => {
|
||||
let csvContent = 'data:text/csv;charset=utf-8,' + response.data;
|
||||
var encodedUri = encodeURI(csvContent);
|
||||
var downloadLink = document.createElement('a');
|
||||
downloadLink.href = encodedUri;
|
||||
downloadLink.download = reportObj.fileName;
|
||||
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
// document.body.removeChild(downloadLink);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
|
||||
@@ -5,7 +5,17 @@ global.open = jest.fn();
|
||||
global.axios = axios;
|
||||
jest.mock('axios');
|
||||
|
||||
const createElementSpy = () => {
|
||||
const element = document.createElement('a');
|
||||
jest.spyOn(document, 'createElement').mockImplementation(() => element);
|
||||
return element;
|
||||
};
|
||||
|
||||
describe('#actions', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('#downloadAgentReports', () => {
|
||||
it('open CSV download prompt if API is success', async () => {
|
||||
axios.get.mockResolvedValue({
|
||||
@@ -17,15 +27,55 @@ describe('#actions', () => {
|
||||
to: 1630504922510,
|
||||
fileName: 'agent-report-01-09-2021.csv',
|
||||
};
|
||||
const mockDownloadElement = document.createElement('a');
|
||||
jest
|
||||
.spyOn(document, 'createElement')
|
||||
.mockImplementation(() => mockDownloadElement);
|
||||
const mockAgentDownloadElement = createElementSpy();
|
||||
await actions.downloadAgentReports(1, param);
|
||||
expect(mockDownloadElement.href).toEqual(
|
||||
expect(mockAgentDownloadElement.href).toEqual(
|
||||
'data:text/csv;charset=utf-8,Agent%20name,Conversations%20count,Avg%20first%20response%20time%20(Minutes),Avg%20resolution%20time%20(Minutes)%0A%20%20%20%20%20%20%20%20Pranav,36,114,28411'
|
||||
);
|
||||
expect(mockDownloadElement.download).toEqual(param.fileName);
|
||||
expect(mockAgentDownloadElement.download).toEqual(param.fileName);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#downloadLabelReports', () => {
|
||||
it('open CSV download prompt if API is success', async () => {
|
||||
axios.get.mockResolvedValue({
|
||||
data: `Label Title,Conversations count,Avg first response time (Minutes),Avg resolution time (Minutes)
|
||||
website,0,0,0`,
|
||||
});
|
||||
const param = {
|
||||
from: 1632335400,
|
||||
to: 1632853800,
|
||||
type: 'label',
|
||||
fileName: 'label-report-01-09-2021.csv',
|
||||
};
|
||||
const mockLabelDownloadElement = createElementSpy();
|
||||
await actions.downloadLabelReports(1, param);
|
||||
expect(mockLabelDownloadElement.href).toEqual(
|
||||
'data:text/csv;charset=utf-8,Label%20Title,Conversations%20count,Avg%20first%20response%20time%20(Minutes),Avg%20resolution%20time%20(Minutes)%0A%20%20%20%20%20%20%20%20website,0,0,0'
|
||||
);
|
||||
expect(mockLabelDownloadElement.download).toEqual(param.fileName);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#downloadInboxReports', () => {
|
||||
it('open CSV download prompt if API is success', async () => {
|
||||
axios.get.mockResolvedValue({
|
||||
data: `Inbox name,Conversations count,Avg first response time (Minutes),Avg resolution time (Minutes)
|
||||
Fayaz,2,127,0
|
||||
EMa,0,0,0
|
||||
Twillio WA,0,0,0`,
|
||||
});
|
||||
const param = {
|
||||
from: 1631039400,
|
||||
to: 1635013800,
|
||||
fileName: 'inbox-report-24-10-2021.csv',
|
||||
};
|
||||
const mockInboxDownloadElement = createElementSpy();
|
||||
await actions.downloadInboxReports(1, param);
|
||||
expect(mockInboxDownloadElement.href).toEqual(
|
||||
'data:text/csv;charset=utf-8,Inbox%20name,Conversations%20count,Avg%20first%20response%20time%20(Minutes),Avg%20resolution%20time%20(Minutes)%0A%20%20%20%20%20%20%20%20Fayaz,2,127,0%0A%20%20%20%20%20%20%20%20EMa,0,0,0%0A%20%20%20%20%20%20%20%20Twillio%20WA,0,0,0'
|
||||
);
|
||||
expect(mockInboxDownloadElement.download).toEqual(param.fileName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user