Merge branch 'develop' into feat/new-auth-screens
This commit is contained in:
commit
1107e2c4fb
96 changed files with 2038 additions and 341 deletions
|
@ -12,8 +12,8 @@ defaults: &defaults
|
||||||
# Specify service dependencies here if necessary
|
# Specify service dependencies here if necessary
|
||||||
# CircleCI maintains a library of pre-built images
|
# CircleCI maintains a library of pre-built images
|
||||||
# documented at https://circleci.com/docs/2.0/circleci-images/
|
# documented at https://circleci.com/docs/2.0/circleci-images/
|
||||||
- image: circleci/postgres:alpine
|
- image: cimg/postgres:14.1
|
||||||
- image: circleci/redis:alpine
|
- image: cimg/redis:6.2.6
|
||||||
environment:
|
environment:
|
||||||
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
|
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
|
||||||
- RAILS_LOG_TO_STDOUT: false
|
- RAILS_LOG_TO_STDOUT: false
|
||||||
|
@ -110,7 +110,7 @@ jobs:
|
||||||
- run:
|
- run:
|
||||||
name: Run backend tests
|
name: Run backend tests
|
||||||
command: |
|
command: |
|
||||||
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10
|
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10 --format documentation
|
||||||
~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
|
~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
root: ~/tmp
|
root: ~/tmp
|
||||||
|
|
|
@ -70,7 +70,7 @@ class ContactBuilder
|
||||||
update_contact_avatar(contact)
|
update_contact_avatar(contact)
|
||||||
contact_inbox
|
contact_inbox
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.info e
|
Rails.logger.error e
|
||||||
raise e
|
raise e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
end
|
end
|
||||||
ensure_contact_avatar
|
ensure_contact_avatar
|
||||||
rescue Koala::Facebook::AuthenticationError
|
rescue Koala::Facebook::AuthenticationError
|
||||||
Rails.logger.info "Facebook Authorization expired for Inbox #{@inbox.id}"
|
Rails.logger.error "Facebook Authorization expired for Inbox #{@inbox.id}"
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Sentry.capture_exception(e)
|
Sentry.capture_exception(e)
|
||||||
true
|
true
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
class V2::ReportBuilder
|
class V2::ReportBuilder
|
||||||
include DateRangeHelper
|
include DateRangeHelper
|
||||||
|
include ReportHelper
|
||||||
attr_reader :account, :params
|
attr_reader :account, :params
|
||||||
|
|
||||||
DEFAULT_GROUP_BY = 'day'.freeze
|
DEFAULT_GROUP_BY = 'day'.freeze
|
||||||
|
@ -18,8 +19,14 @@ class V2::ReportBuilder
|
||||||
|
|
||||||
# For backward compatible with old report
|
# For backward compatible with old report
|
||||||
def build
|
def build
|
||||||
timeseries.each_with_object([]) do |p, arr|
|
if %w[avg_first_response_time avg_resolution_time].include?(params[:metric])
|
||||||
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i }
|
timeseries.each_with_object([]) do |p, arr|
|
||||||
|
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i, count: @grouped_values.count[p[0]] }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
timeseries.each_with_object([]) do |p, arr|
|
||||||
|
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -34,23 +41,16 @@ class V2::ReportBuilder
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def conversation_metrics
|
||||||
|
if params[:type].equal?(:account)
|
||||||
def scope
|
conversations
|
||||||
case params[:type]
|
else
|
||||||
when :account
|
agent_metrics
|
||||||
account
|
|
||||||
when :inbox
|
|
||||||
inbox
|
|
||||||
when :agent
|
|
||||||
user
|
|
||||||
when :label
|
|
||||||
label
|
|
||||||
when :team
|
|
||||||
team
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def inbox
|
def inbox
|
||||||
@inbox ||= account.inboxes.find(params[:id])
|
@inbox ||= account.inboxes.find(params[:id])
|
||||||
end
|
end
|
||||||
|
@ -68,7 +68,7 @@ class V2::ReportBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_grouped_values(object_scope)
|
def get_grouped_values(object_scope)
|
||||||
object_scope.group_by_period(
|
@grouped_values = object_scope.group_by_period(
|
||||||
params[:group_by] || DEFAULT_GROUP_BY,
|
params[:group_by] || DEFAULT_GROUP_BY,
|
||||||
:created_at,
|
:created_at,
|
||||||
default_value: 0,
|
default_value: 0,
|
||||||
|
@ -78,47 +78,26 @@ class V2::ReportBuilder
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversations_count
|
def agent_metrics
|
||||||
(get_grouped_values scope.conversations).count
|
users = @account.users
|
||||||
|
users = users.where(id: params[:user_id]) if params[:user_id].present?
|
||||||
|
users.each_with_object([]) do |user, arr|
|
||||||
|
@user = user
|
||||||
|
arr << {
|
||||||
|
user: { id: user.id, name: user.name, thumbnail: user.avatar_url },
|
||||||
|
metric: conversations
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def incoming_messages_count
|
def conversations
|
||||||
(get_grouped_values scope.messages.incoming.unscope(:order)).count
|
@open_conversations = scope.conversations.open
|
||||||
end
|
first_response_count = scope.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
|
||||||
|
metric = {
|
||||||
def outgoing_messages_count
|
open: @open_conversations.count,
|
||||||
(get_grouped_values scope.messages.outgoing.unscope(:order)).count
|
unattended: @open_conversations.count - first_response_count
|
||||||
end
|
}
|
||||||
|
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
|
||||||
def resolutions_count
|
metric
|
||||||
(get_grouped_values scope.conversations.resolved).count
|
|
||||||
end
|
|
||||||
|
|
||||||
def avg_first_response_time
|
|
||||||
(get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value)
|
|
||||||
end
|
|
||||||
|
|
||||||
def avg_resolution_time
|
|
||||||
(get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value)
|
|
||||||
end
|
|
||||||
|
|
||||||
def avg_resolution_time_summary
|
|
||||||
avg_rt = scope.reporting_events
|
|
||||||
.where(name: 'conversation_resolved', created_at: range)
|
|
||||||
.average(:value)
|
|
||||||
|
|
||||||
return 0 if avg_rt.blank?
|
|
||||||
|
|
||||||
avg_rt
|
|
||||||
end
|
|
||||||
|
|
||||||
def avg_first_response_time_summary
|
|
||||||
avg_frt = scope.reporting_events
|
|
||||||
.where(name: 'first_response', created_at: range)
|
|
||||||
.average(:value)
|
|
||||||
|
|
||||||
return 0 if avg_frt.blank?
|
|
||||||
|
|
||||||
avg_frt
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,13 +7,22 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@automation_rule = Current.account.automation_rules.create(automation_rules_permit)
|
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
|
||||||
|
@automation_rule.actions = params[:actions]
|
||||||
|
|
||||||
|
render json: { error: @automation_rule.errors.messages }, status: :unprocessable_entity and return unless @automation_rule.valid?
|
||||||
|
|
||||||
|
@automation_rule.save!
|
||||||
|
process_attachments
|
||||||
|
@automation_rule
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@automation_rule.update(automation_rules_permit)
|
@automation_rule.update(automation_rules_permit)
|
||||||
|
process_attachments
|
||||||
|
@automation_rule
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -30,11 +39,19 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def process_attachments
|
||||||
|
return if params[:attachments].blank?
|
||||||
|
|
||||||
|
params[:attachments].each do |uploaded_attachment|
|
||||||
|
@automation_rule.files.attach(uploaded_attachment)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def automation_rules_permit
|
def automation_rules_permit
|
||||||
params.permit(
|
params.permit(
|
||||||
:name, :description, :event_name, :account_id, :active,
|
:name, :description, :event_name, :account_id, :active,
|
||||||
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
|
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
|
||||||
actions: [:action_name, { action_params: [{}] }]
|
actions: [:action_name, { action_params: [] }]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||||
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
|
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
|
||||||
koala.exchange_access_token_info(omniauth_token)['access_token']
|
koala.exchange_access_token_info(omniauth_token)['access_token']
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.info e
|
Rails.logger.error e
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_already_existing_facebook_pages(data)
|
def mark_already_existing_facebook_pages(data)
|
||||||
|
|
|
@ -35,6 +35,12 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||||
render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv'
|
render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conversations
|
||||||
|
return head :unprocessable_entity if params[:type].blank?
|
||||||
|
|
||||||
|
render json: conversation_metrics
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def check_authorization
|
def check_authorization
|
||||||
|
@ -73,6 +79,13 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conversation_params
|
||||||
|
{
|
||||||
|
type: params[:type].to_sym,
|
||||||
|
user_id: params[:user_id]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def range
|
def range
|
||||||
{
|
{
|
||||||
current: {
|
current: {
|
||||||
|
@ -91,4 +104,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||||
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary
|
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary
|
||||||
summary
|
summary
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conversation_metrics
|
||||||
|
V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,7 @@ class Twitter::CallbacksController < Twitter::BaseController
|
||||||
redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
||||||
end
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.info e
|
Rails.logger.error e
|
||||||
redirect_to twitter_app_redirect_url
|
redirect_to twitter_app_redirect_url
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ class Webhooks::InstagramController < ApplicationController
|
||||||
::Webhooks::InstagramEventsJob.perform_later(params.to_unsafe_hash[:entry])
|
::Webhooks::InstagramEventsJob.perform_later(params.to_unsafe_hash[:entry])
|
||||||
render json: :ok
|
render json: :ok
|
||||||
else
|
else
|
||||||
Rails.logger.info("Message is not received from the instagram webhook event: #{params['object']}")
|
Rails.logger.warn("Message is not received from the instagram webhook event: #{params['object']}")
|
||||||
head :unprocessable_entity
|
head :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
62
app/helpers/report_helper.rb
Normal file
62
app/helpers/report_helper.rb
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
module ReportHelper
|
||||||
|
private
|
||||||
|
|
||||||
|
def scope
|
||||||
|
case params[:type]
|
||||||
|
when :account
|
||||||
|
account
|
||||||
|
when :inbox
|
||||||
|
inbox
|
||||||
|
when :agent
|
||||||
|
user
|
||||||
|
when :label
|
||||||
|
label
|
||||||
|
when :team
|
||||||
|
team
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversations_count
|
||||||
|
(get_grouped_values scope.conversations).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def incoming_messages_count
|
||||||
|
(get_grouped_values scope.messages.incoming.unscope(:order)).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def outgoing_messages_count
|
||||||
|
(get_grouped_values scope.messages.outgoing.unscope(:order)).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolutions_count
|
||||||
|
(get_grouped_values scope.conversations.resolved).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def avg_first_response_time
|
||||||
|
(get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def avg_resolution_time
|
||||||
|
(get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def avg_resolution_time_summary
|
||||||
|
avg_rt = scope.reporting_events
|
||||||
|
.where(name: 'conversation_resolved', created_at: range)
|
||||||
|
.average(:value)
|
||||||
|
|
||||||
|
return 0 if avg_rt.blank?
|
||||||
|
|
||||||
|
avg_rt
|
||||||
|
end
|
||||||
|
|
||||||
|
def avg_first_response_time_summary
|
||||||
|
avg_frt = scope.reporting_events
|
||||||
|
.where(name: 'first_response', created_at: range)
|
||||||
|
.average(:value)
|
||||||
|
|
||||||
|
return 0 if avg_frt.blank?
|
||||||
|
|
||||||
|
avg_frt
|
||||||
|
end
|
||||||
|
end
|
|
@ -18,6 +18,13 @@
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
font-weight: $font-weight-bold;
|
font-weight: $font-weight-bold;
|
||||||
color: $color-heading;
|
color: $color-heading;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
color: var(--b-400);
|
||||||
|
margin-left: var(--space-micro);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-wrap {
|
.metric-wrap {
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
<select
|
<select
|
||||||
v-model="action_name"
|
v-model="action_name"
|
||||||
class="action__question"
|
class="action__question"
|
||||||
@change="resetFilter()"
|
:class="{ 'full-width': !inputType }"
|
||||||
|
@change="resetAction()"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="attribute in actionTypes"
|
v-for="attribute in actionTypes"
|
||||||
|
@ -18,19 +19,38 @@
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="filter__answer--wrap">
|
<div class="filter__answer--wrap">
|
||||||
<div class="multiselect-wrap--small">
|
<div v-if="inputType">
|
||||||
<multiselect
|
<div
|
||||||
|
v-if="inputType === 'multi_select'"
|
||||||
|
class="multiselect-wrap--small"
|
||||||
|
>
|
||||||
|
<multiselect
|
||||||
|
v-model="action_params"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:placeholder="'Select'"
|
||||||
|
:multiple="true"
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
deselect-label=""
|
||||||
|
:max-height="160"
|
||||||
|
:options="dropdownValues"
|
||||||
|
:allow-empty="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-else-if="inputType === 'email'"
|
||||||
v-model="action_params"
|
v-model="action_params"
|
||||||
track-by="id"
|
type="email"
|
||||||
label="name"
|
class="answer--text-input"
|
||||||
:placeholder="'Select'"
|
placeholder="Enter email"
|
||||||
:multiple="true"
|
/>
|
||||||
selected-label
|
<input
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
v-else-if="inputType === 'url'"
|
||||||
deselect-label=""
|
v-model="action_params"
|
||||||
:max-height="160"
|
type="url"
|
||||||
:options="dropdownValues"
|
class="answer--text-input"
|
||||||
:allow-empty="false"
|
placeholder="Enter url"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -91,13 +111,17 @@ export default {
|
||||||
this.$emit('input', { ...payload, action_params: value });
|
this.$emit('input', { ...payload, action_params: value });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
inputType() {
|
||||||
|
return this.actionTypes.find(action => action.key === this.action_name)
|
||||||
|
.inputType;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removeAction() {
|
removeAction() {
|
||||||
this.$emit('removeAction');
|
this.$emit('removeAction');
|
||||||
},
|
},
|
||||||
resetFilter() {
|
resetAction() {
|
||||||
this.$emit('resetFilter');
|
this.$emit('resetAction');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -136,6 +160,10 @@ export default {
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action__question.full-width {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.filter__answer--wrap {
|
.filter__answer--wrap {
|
||||||
margin-right: var(--space-smaller);
|
margin-right: var(--space-smaller);
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
|
@ -5,7 +5,14 @@
|
||||||
@click="onClick(index)"
|
@click="onClick(index)"
|
||||||
>
|
>
|
||||||
<h3 class="heading">
|
<h3 class="heading">
|
||||||
{{ heading }}
|
<span>{{ heading }}</span>
|
||||||
|
<fluent-icon
|
||||||
|
v-if="infoText"
|
||||||
|
v-tooltip="infoText"
|
||||||
|
size="14"
|
||||||
|
icon="info"
|
||||||
|
class="info-icon"
|
||||||
|
/>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="metric-wrap">
|
<div class="metric-wrap">
|
||||||
<h4 class="metric">
|
<h4 class="metric">
|
||||||
|
@ -22,6 +29,7 @@
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
heading: { type: String, default: '' },
|
heading: { type: String, default: '' },
|
||||||
|
infoText: { type: String, default: '' },
|
||||||
point: { type: [Number, String], default: '' },
|
point: { type: [Number, String], default: '' },
|
||||||
trend: { type: Number, default: null },
|
trend: { type: Number, default: null },
|
||||||
index: { type: Number, default: null },
|
index: { type: Number, default: null },
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Bar } from 'vue-chartjs';
|
||||||
const fontFamily =
|
const fontFamily =
|
||||||
'-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
'-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
||||||
|
|
||||||
const chartOptions = {
|
const defaultChartOptions = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
legend: {
|
legend: {
|
||||||
|
@ -11,10 +11,14 @@ const chartOptions = {
|
||||||
fontFamily,
|
fontFamily,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
datasets: {
|
||||||
|
bar: {
|
||||||
|
barPercentage: 1.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
scales: {
|
scales: {
|
||||||
xAxes: [
|
xAxes: [
|
||||||
{
|
{
|
||||||
barPercentage: 1.1,
|
|
||||||
ticks: {
|
ticks: {
|
||||||
fontFamily,
|
fontFamily,
|
||||||
},
|
},
|
||||||
|
@ -39,8 +43,20 @@ const chartOptions = {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: Bar,
|
extends: Bar,
|
||||||
props: ['collection'],
|
props: {
|
||||||
|
collection: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
chartOptions: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.renderChart(this.collection, chartOptions);
|
this.renderChart(this.collection, {
|
||||||
|
...defaultChartOptions,
|
||||||
|
...this.chartOptions,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,6 +22,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||||
'contact.deleted': this.onContactDelete,
|
'contact.deleted': this.onContactDelete,
|
||||||
'contact.updated': this.onContactUpdate,
|
'contact.updated': this.onContactUpdate,
|
||||||
'conversation.mentioned': this.onConversationMentioned,
|
'conversation.mentioned': this.onConversationMentioned,
|
||||||
|
'notification.created': this.onNotificationCreated,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,6 +135,10 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||||
onContactUpdate = data => {
|
onContactUpdate = data => {
|
||||||
this.app.$store.dispatch('contacts/updateContact', data);
|
this.app.$store.dispatch('contacts/updateContact', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onNotificationCreated = data => {
|
||||||
|
this.app.$store.dispatch('notifications/addNotification', data);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -70,6 +70,14 @@
|
||||||
"SUCCESS_MESSAGE": "Contacts saved successfully",
|
"SUCCESS_MESSAGE": "Contacts saved successfully",
|
||||||
"ERROR_MESSAGE": "There was an error, please try again"
|
"ERROR_MESSAGE": "There was an error, please try again"
|
||||||
},
|
},
|
||||||
|
"DELETE_NOTE": {
|
||||||
|
"CONFIRM":{
|
||||||
|
"TITLE": "Confirm Deletion",
|
||||||
|
"MESSAGE": "Are you want sure to delete this note?",
|
||||||
|
"YES": "Yes, Delete it",
|
||||||
|
"NO": "No, Keep it"
|
||||||
|
}
|
||||||
|
},
|
||||||
"DELETE_CONTACT": {
|
"DELETE_CONTACT": {
|
||||||
"BUTTON_LABEL": "Delete Contact",
|
"BUTTON_LABEL": "Delete Contact",
|
||||||
"TITLE": "Delete contact",
|
"TITLE": "Delete contact",
|
||||||
|
|
|
@ -18,12 +18,14 @@
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"FIRST_RESPONSE_TIME": {
|
"FIRST_RESPONSE_TIME": {
|
||||||
"NAME": "First response time",
|
"NAME": "First Response Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||||
},
|
},
|
||||||
"RESOLUTION_TIME": {
|
"RESOLUTION_TIME": {
|
||||||
"NAME": "Resolution Time",
|
"NAME": "Resolution Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||||
},
|
},
|
||||||
"RESOLUTION_COUNT": {
|
"RESOLUTION_COUNT": {
|
||||||
"NAME": "Resolution Count",
|
"NAME": "Resolution Count",
|
||||||
|
@ -98,12 +100,14 @@
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"FIRST_RESPONSE_TIME": {
|
"FIRST_RESPONSE_TIME": {
|
||||||
"NAME": "First response time",
|
"NAME": "First Response Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||||
},
|
},
|
||||||
"RESOLUTION_TIME": {
|
"RESOLUTION_TIME": {
|
||||||
"NAME": "Resolution Time",
|
"NAME": "Resolution Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||||
},
|
},
|
||||||
"RESOLUTION_COUNT": {
|
"RESOLUTION_COUNT": {
|
||||||
"NAME": "Resolution Count",
|
"NAME": "Resolution Count",
|
||||||
|
@ -161,12 +165,14 @@
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"FIRST_RESPONSE_TIME": {
|
"FIRST_RESPONSE_TIME": {
|
||||||
"NAME": "First response time",
|
"NAME": "First Response Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||||
},
|
},
|
||||||
"RESOLUTION_TIME": {
|
"RESOLUTION_TIME": {
|
||||||
"NAME": "Resolution Time",
|
"NAME": "Resolution Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||||
},
|
},
|
||||||
"RESOLUTION_COUNT": {
|
"RESOLUTION_COUNT": {
|
||||||
"NAME": "Resolution Count",
|
"NAME": "Resolution Count",
|
||||||
|
@ -224,12 +230,14 @@
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"FIRST_RESPONSE_TIME": {
|
"FIRST_RESPONSE_TIME": {
|
||||||
"NAME": "First response time",
|
"NAME": "First Response Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||||
},
|
},
|
||||||
"RESOLUTION_TIME": {
|
"RESOLUTION_TIME": {
|
||||||
"NAME": "Resolution Time",
|
"NAME": "Resolution Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||||
},
|
},
|
||||||
"RESOLUTION_COUNT": {
|
"RESOLUTION_COUNT": {
|
||||||
"NAME": "Resolution Count",
|
"NAME": "Resolution Count",
|
||||||
|
@ -287,12 +295,14 @@
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"FIRST_RESPONSE_TIME": {
|
"FIRST_RESPONSE_TIME": {
|
||||||
"NAME": "First response time",
|
"NAME": "First Response Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||||
},
|
},
|
||||||
"RESOLUTION_TIME": {
|
"RESOLUTION_TIME": {
|
||||||
"NAME": "Resolution Time",
|
"NAME": "Resolution Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||||
},
|
},
|
||||||
"RESOLUTION_COUNT": {
|
"RESOLUTION_COUNT": {
|
||||||
"NAME": "Resolution Count",
|
"NAME": "Resolution Count",
|
||||||
|
|
|
@ -5,6 +5,7 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
accountSummary: 'getAccountSummary',
|
accountSummary: 'getAccountSummary',
|
||||||
|
accountReport: 'getAccountReports',
|
||||||
}),
|
}),
|
||||||
calculateTrend() {
|
calculateTrend() {
|
||||||
return metric_key => {
|
return metric_key => {
|
||||||
|
@ -19,15 +20,32 @@ export default {
|
||||||
},
|
},
|
||||||
displayMetric() {
|
displayMetric() {
|
||||||
return metric_key => {
|
return metric_key => {
|
||||||
if (
|
if (this.isAverageMetricType(metric_key)) {
|
||||||
['avg_first_response_time', 'avg_resolution_time'].includes(
|
|
||||||
metric_key
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return formatTime(this.accountSummary[metric_key]);
|
return formatTime(this.accountSummary[metric_key]);
|
||||||
}
|
}
|
||||||
return this.accountSummary[metric_key];
|
return this.accountSummary[metric_key];
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
displayInfoText() {
|
||||||
|
return metric_key => {
|
||||||
|
if (this.metrics[this.currentSelection].KEY !== metric_key) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (this.isAverageMetricType(metric_key)) {
|
||||||
|
const total = this.accountReport.data
|
||||||
|
.map(item => item.count)
|
||||||
|
.reduce((prev, curr) => prev + curr, 0);
|
||||||
|
return `${this.metrics[this.currentSelection].INFO_TEXT} ${total}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
},
|
||||||
|
isAverageMetricType() {
|
||||||
|
return metric_key => {
|
||||||
|
return ['avg_first_response_time', 'avg_resolution_time'].includes(
|
||||||
|
metric_key
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,6 +11,7 @@ describe('reportMixin', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getters = {
|
getters = {
|
||||||
getAccountSummary: () => reportFixtures.summary,
|
getAccountSummary: () => reportFixtures.summary,
|
||||||
|
getAccountReports: () => reportFixtures.report,
|
||||||
};
|
};
|
||||||
store = new Vuex.Store({ getters });
|
store = new Vuex.Store({ getters });
|
||||||
});
|
});
|
||||||
|
@ -38,4 +39,67 @@ describe('reportMixin', () => {
|
||||||
expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(25);
|
expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(25);
|
||||||
expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0);
|
expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('display info text', () => {
|
||||||
|
const Component = {
|
||||||
|
render() {},
|
||||||
|
title: 'TestComponent',
|
||||||
|
mixins: [reportMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentSelection: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
metrics() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
DESC: '( Avg )',
|
||||||
|
INFO_TEXT: 'Total number of conversations used for computation:',
|
||||||
|
KEY: 'avg_first_response_time',
|
||||||
|
NAME: 'First Response Time',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
|
expect(wrapper.vm.displayInfoText('avg_first_response_time')).toEqual(
|
||||||
|
'Total number of conversations used for computation: 4'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('do not display info text', () => {
|
||||||
|
const Component = {
|
||||||
|
render() {},
|
||||||
|
title: 'TestComponent',
|
||||||
|
mixins: [reportMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentSelection: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
metrics() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
DESC: '( Total )',
|
||||||
|
INFO_TEXT: '',
|
||||||
|
KEY: 'conversation_count',
|
||||||
|
NAME: 'Conversations',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DESC: '( Avg )',
|
||||||
|
INFO_TEXT: 'Total number of conversations used for computation:',
|
||||||
|
KEY: 'avg_first_response_time',
|
||||||
|
NAME: 'First Response Time',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
|
expect(wrapper.vm.displayInfoText('conversation_count')).toEqual('');
|
||||||
|
expect(wrapper.vm.displayInfoText('incoming_messages_count')).toEqual('');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,4 +15,15 @@ export default {
|
||||||
},
|
},
|
||||||
resolutions_count: 3,
|
resolutions_count: 3,
|
||||||
},
|
},
|
||||||
|
report: {
|
||||||
|
data: [
|
||||||
|
{ value: '0.00', timestamp: 1647541800, count: 0 },
|
||||||
|
{ value: '0.00', timestamp: 1647628200, count: 0 },
|
||||||
|
{ value: '0.00', timestamp: 1647714600, count: 0 },
|
||||||
|
{ value: '0.00', timestamp: 1647801000, count: 0 },
|
||||||
|
{ value: '0.01', timestamp: 1647887400, count: 4 },
|
||||||
|
{ value: '0.00', timestamp: 1647973800, count: 0 },
|
||||||
|
{ value: '0.00', timestamp: 1648060200, count: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,9 +21,19 @@
|
||||||
size="tiny"
|
size="tiny"
|
||||||
icon="delete"
|
icon="delete"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
@click="onDelete"
|
@click="toggleDeleteModal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<woot-delete-modal
|
||||||
|
v-if="showDeleteModal"
|
||||||
|
:show.sync="showDeleteModal"
|
||||||
|
:on-close="closeDelete"
|
||||||
|
:on-confirm="confirmDeletion"
|
||||||
|
:title="$t('DELETE_NOTE.CONFIRM.TITLE')"
|
||||||
|
:message="$t('DELETE_NOTE.CONFIRM.MESSAGE')"
|
||||||
|
:confirm-text="$t('DELETE_NOTE.CONFIRM.YES')"
|
||||||
|
:reject-text="$t('DELETE_NOTE.CONFIRM.NO')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="note__content" v-html="formatMessage(note || '')" />
|
<p class="note__content" v-html="formatMessage(note || '')" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,7 +69,11 @@ export default {
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showDeleteModal: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
readableTime() {
|
readableTime() {
|
||||||
return this.dynamicTime(this.createdAt);
|
return this.dynamicTime(this.createdAt);
|
||||||
|
@ -73,9 +87,19 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleDeleteModal() {
|
||||||
|
this.showDeleteModal = !this.showDeleteModal;
|
||||||
|
},
|
||||||
onDelete() {
|
onDelete() {
|
||||||
this.$emit('delete', this.id);
|
this.$emit('delete', this.id);
|
||||||
},
|
},
|
||||||
|
confirmDeletion() {
|
||||||
|
this.onDelete();
|
||||||
|
this.closeDelete();
|
||||||
|
},
|
||||||
|
closeDelete() {
|
||||||
|
this.showDeleteModal = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -101,6 +101,7 @@
|
||||||
getActionDropdownValues(automation.actions[i].action_name)
|
getActionDropdownValues(automation.actions[i].action_name)
|
||||||
"
|
"
|
||||||
:v="$v.automation.actions.$each[i]"
|
:v="$v.automation.actions.$each[i]"
|
||||||
|
@resetAction="resetAction(i)"
|
||||||
@removeAction="removeAction(i)"
|
@removeAction="removeAction(i)"
|
||||||
/>
|
/>
|
||||||
<div class="filter-actions">
|
<div class="filter-actions">
|
||||||
|
@ -187,7 +188,13 @@ export default {
|
||||||
required,
|
required,
|
||||||
$each: {
|
$each: {
|
||||||
action_params: {
|
action_params: {
|
||||||
required,
|
required: requiredIf(prop => {
|
||||||
|
return !(
|
||||||
|
prop.action_name === 'mute_conversation' ||
|
||||||
|
prop.action_name === 'snooze_convresation' ||
|
||||||
|
prop.action_name === 'resolve_convresation'
|
||||||
|
);
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -351,7 +358,6 @@ export default {
|
||||||
getActionDropdownValues(type) {
|
getActionDropdownValues(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'assign_team':
|
case 'assign_team':
|
||||||
case 'send_email_to_team':
|
|
||||||
return this.$store.getters['teams/getTeams'];
|
return this.$store.getters['teams/getTeams'];
|
||||||
case 'add_label':
|
case 'add_label':
|
||||||
return this.$store.getters['labels/getLabels'].map(i => {
|
return this.$store.getters['labels/getLabels'].map(i => {
|
||||||
|
@ -424,6 +430,9 @@ export default {
|
||||||
).filterOperators[0].value;
|
).filterOperators[0].value;
|
||||||
this.automation.conditions[index].values = '';
|
this.automation.conditions[index].values = '';
|
||||||
},
|
},
|
||||||
|
resetAction(index) {
|
||||||
|
this.automation.actions[index].action_params = [];
|
||||||
|
},
|
||||||
showUserInput(operatorType) {
|
showUserInput(operatorType) {
|
||||||
if (operatorType === 'is_present' || operatorType === 'is_not_present')
|
if (operatorType === 'is_present' || operatorType === 'is_not_present')
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -351,7 +351,6 @@ export default {
|
||||||
getActionDropdownValues(type) {
|
getActionDropdownValues(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'assign_team':
|
case 'assign_team':
|
||||||
case 'send_email_to_team':
|
|
||||||
return this.$store.getters['teams/getTeams'];
|
return this.$store.getters['teams/getTeams'];
|
||||||
case 'add_label':
|
case 'add_label':
|
||||||
return this.$store.getters['labels/getLabels'].map(i => {
|
return this.$store.getters['labels/getLabels'].map(i => {
|
||||||
|
|
|
@ -79,8 +79,33 @@ export const AUTOMATIONS = {
|
||||||
// {
|
// {
|
||||||
// key: 'send_email_to_team',
|
// key: 'send_email_to_team',
|
||||||
// name: 'Send an email to team',
|
// name: 'Send an email to team',
|
||||||
// attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
|
// attributeI18nKey: 'SEND_MESSAGE',
|
||||||
// },
|
// },
|
||||||
|
{
|
||||||
|
key: 'send_email_transcript',
|
||||||
|
name: 'Send an email transcript',
|
||||||
|
attributeI18nKey: 'SEND_EMAIL_TRANSCRIPT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mute_conversation',
|
||||||
|
name: 'Mute conversation',
|
||||||
|
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'snooze_convresation',
|
||||||
|
name: 'Snooze conversation',
|
||||||
|
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'resolve_convresation',
|
||||||
|
name: 'Resolve conversation',
|
||||||
|
attributeI18nKey: 'RESOLVE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_webhook_event',
|
||||||
|
name: 'Send Webhook Event',
|
||||||
|
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
conversation_created: {
|
conversation_created: {
|
||||||
|
@ -120,15 +145,40 @@ export const AUTOMATIONS = {
|
||||||
name: 'Assign a team',
|
name: 'Assign a team',
|
||||||
attributeI18nKey: 'ASSIGN_TEAM',
|
attributeI18nKey: 'ASSIGN_TEAM',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'assign_agent',
|
||||||
|
name: 'Assign an agent',
|
||||||
|
attributeI18nKey: 'ASSIGN_AGENT',
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// key: 'send_email_to_team',
|
// key: 'send_email_to_team',
|
||||||
// name: 'Send an email to team',
|
// name: 'Send an email to team',
|
||||||
// attributeI18nKey: 'SEND_MESSAGE',
|
// attributeI18nKey: 'SEND_MESSAGE',
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
key: 'assign_agent',
|
key: 'send_email_transcript',
|
||||||
name: 'Assign an agent',
|
name: 'Send an email transcript',
|
||||||
attributeI18nKey: 'ASSIGN_AGENT',
|
attributeI18nKey: 'SEND_EMAIL_TRANSCRIPT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mute_conversation',
|
||||||
|
name: 'Mute conversation',
|
||||||
|
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'snooze_convresation',
|
||||||
|
name: 'Snooze conversation',
|
||||||
|
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'resolve_convresation',
|
||||||
|
name: 'Resolve conversation',
|
||||||
|
attributeI18nKey: 'RESOLVE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_webhook_event',
|
||||||
|
name: 'Send Webhook Event',
|
||||||
|
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -183,16 +233,40 @@ export const AUTOMATIONS = {
|
||||||
name: 'Assign a team',
|
name: 'Assign a team',
|
||||||
attributeI18nKey: 'ASSIGN_TEAM',
|
attributeI18nKey: 'ASSIGN_TEAM',
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// key: 'send_email_to_team',
|
|
||||||
// name: 'Send an email to team',
|
|
||||||
// attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
key: 'assign_agent',
|
key: 'assign_agent',
|
||||||
name: 'Assign an agent',
|
name: 'Assign an agent',
|
||||||
attributeI18nKey: 'ASSIGN_AGENT',
|
attributeI18nKey: 'ASSIGN_AGENT',
|
||||||
attributeKey: 'assignee_id',
|
},
|
||||||
|
// {
|
||||||
|
// key: 'send_email_to_team',
|
||||||
|
// name: 'Send an email to team',
|
||||||
|
// attributeI18nKey: 'SEND_MESSAGE',
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
key: 'send_email_transcript',
|
||||||
|
name: 'Send an email transcript',
|
||||||
|
attributeI18nKey: 'SEND_EMAIL_TRANSCRIPT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mute_conversation',
|
||||||
|
name: 'Mute conversation',
|
||||||
|
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'snooze_convresation',
|
||||||
|
name: 'Snooze conversation',
|
||||||
|
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'resolve_convresation',
|
||||||
|
name: 'Resolve conversation',
|
||||||
|
attributeI18nKey: 'RESOLVE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_webhook_event',
|
||||||
|
name: 'Send Webhook Event',
|
||||||
|
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -217,13 +291,41 @@ export const AUTOMATION_ACTION_TYPES = [
|
||||||
{
|
{
|
||||||
key: 'assign_team',
|
key: 'assign_team',
|
||||||
label: 'Assign a team',
|
label: 'Assign a team',
|
||||||
|
inputType: 'multi_select',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'add_label',
|
key: 'add_label',
|
||||||
label: 'Add a label',
|
label: 'Add a label',
|
||||||
|
inputType: 'multi_select',
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// key: 'send_email_to_team',
|
// key: 'send_email_to_team',
|
||||||
// label: 'Send an email to team',
|
// label: 'Send an email to team',
|
||||||
|
// inputType: 'multi_select',
|
||||||
// },
|
// },
|
||||||
|
{
|
||||||
|
key: 'send_email_transcript',
|
||||||
|
label: 'Send an email transcript',
|
||||||
|
inputType: 'email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mute_conversation',
|
||||||
|
label: 'Mute conversation',
|
||||||
|
inputType: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'snooze_convresation',
|
||||||
|
label: 'Snooze conversation',
|
||||||
|
inputType: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'resolve_convresation',
|
||||||
|
label: 'Resolve conversation',
|
||||||
|
inputType: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_webhook_event',
|
||||||
|
label: 'Send Webhook Event',
|
||||||
|
inputType: 'url',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
:key="metric.NAME"
|
:key="metric.NAME"
|
||||||
:desc="metric.DESC"
|
:desc="metric.DESC"
|
||||||
:heading="metric.NAME"
|
:heading="metric.NAME"
|
||||||
|
:info-text="displayInfoText(metric.KEY)"
|
||||||
:index="index"
|
:index="index"
|
||||||
:on-click="changeSelection"
|
:on-click="changeSelection"
|
||||||
:point="displayMetric(metric.KEY)"
|
:point="displayMetric(metric.KEY)"
|
||||||
|
@ -35,7 +36,11 @@
|
||||||
:message="$t('REPORT.LOADING_CHART')"
|
:message="$t('REPORT.LOADING_CHART')"
|
||||||
/>
|
/>
|
||||||
<div v-else class="chart-container">
|
<div v-else class="chart-container">
|
||||||
<woot-bar v-if="accountReport.data.length" :collection="collection" />
|
<woot-bar
|
||||||
|
v-if="accountReport.data.length"
|
||||||
|
:collection="collection"
|
||||||
|
:chart-options="chartOptions"
|
||||||
|
/>
|
||||||
<span v-else class="empty-state">
|
<span v-else class="empty-state">
|
||||||
{{ $t('REPORT.NO_ENOUGH_DATA') }}
|
{{ $t('REPORT.NO_ENOUGH_DATA') }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -49,7 +54,7 @@ import { mapGetters } from 'vuex';
|
||||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||||
import format from 'date-fns/format';
|
import format from 'date-fns/format';
|
||||||
import ReportFilterSelector from './components/FilterSelector';
|
import ReportFilterSelector from './components/FilterSelector';
|
||||||
import { GROUP_BY_FILTER } from './constants';
|
import { GROUP_BY_FILTER, METRIC_CHART } from './constants';
|
||||||
import reportMixin from '../../../../mixins/reportMixin';
|
import reportMixin from '../../../../mixins/reportMixin';
|
||||||
|
|
||||||
const REPORTS_KEYS = {
|
const REPORTS_KEYS = {
|
||||||
|
@ -108,16 +113,38 @@ export default {
|
||||||
}
|
}
|
||||||
return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy');
|
return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy');
|
||||||
});
|
});
|
||||||
const data = this.accountReport.data.map(element => element.value);
|
|
||||||
|
const datasets = METRIC_CHART[
|
||||||
|
this.metrics[this.currentSelection].KEY
|
||||||
|
].datasets.map(dataset => {
|
||||||
|
switch (dataset.type) {
|
||||||
|
case 'bar':
|
||||||
|
return {
|
||||||
|
...dataset,
|
||||||
|
yAxisID: 'y-left',
|
||||||
|
label: this.metrics[this.currentSelection].NAME,
|
||||||
|
data: this.accountReport.data.map(element => element.value),
|
||||||
|
};
|
||||||
|
case 'line':
|
||||||
|
return {
|
||||||
|
...dataset,
|
||||||
|
yAxisID: 'y-right',
|
||||||
|
label: this.metrics[0].NAME,
|
||||||
|
data: this.accountReport.data.map(element => element.count),
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return dataset;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
labels,
|
labels,
|
||||||
datasets: [
|
datasets,
|
||||||
{
|
};
|
||||||
label: this.metrics[this.currentSelection].NAME,
|
},
|
||||||
backgroundColor: '#1f93ff',
|
chartOptions() {
|
||||||
data,
|
return {
|
||||||
},
|
scales: METRIC_CHART[this.metrics[this.currentSelection].KEY].scales,
|
||||||
],
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
metrics() {
|
metrics() {
|
||||||
|
@ -133,6 +160,7 @@ export default {
|
||||||
NAME: this.$t(`REPORT.METRICS.${key}.NAME`),
|
NAME: this.$t(`REPORT.METRICS.${key}.NAME`),
|
||||||
KEY: REPORTS_KEYS[key],
|
KEY: REPORTS_KEYS[key],
|
||||||
DESC: this.$t(`REPORT.METRICS.${key}.DESC`),
|
DESC: this.$t(`REPORT.METRICS.${key}.DESC`),
|
||||||
|
INFO_TEXT: this.$t(`REPORT.METRICS.${key}.INFO_TEXT`),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
:key="metric.NAME"
|
:key="metric.NAME"
|
||||||
:desc="metric.DESC"
|
:desc="metric.DESC"
|
||||||
:heading="metric.NAME"
|
:heading="metric.NAME"
|
||||||
|
:info-text="displayInfoText(metric.KEY)"
|
||||||
:index="index"
|
:index="index"
|
||||||
:on-click="changeSelection"
|
:on-click="changeSelection"
|
||||||
:point="displayMetric(metric.KEY)"
|
:point="displayMetric(metric.KEY)"
|
||||||
|
@ -41,6 +42,7 @@
|
||||||
<woot-bar
|
<woot-bar
|
||||||
v-if="accountReport.data.length && filterItemsList.length"
|
v-if="accountReport.data.length && filterItemsList.length"
|
||||||
:collection="collection"
|
:collection="collection"
|
||||||
|
:chart-options="chartOptions"
|
||||||
/>
|
/>
|
||||||
<span v-else class="empty-state">
|
<span v-else class="empty-state">
|
||||||
{{ $t('REPORT.NO_ENOUGH_DATA') }}
|
{{ $t('REPORT.NO_ENOUGH_DATA') }}
|
||||||
|
@ -55,7 +57,7 @@
|
||||||
import ReportFilters from './ReportFilters';
|
import ReportFilters from './ReportFilters';
|
||||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||||
import format from 'date-fns/format';
|
import format from 'date-fns/format';
|
||||||
import { GROUP_BY_FILTER } from '../constants';
|
import { GROUP_BY_FILTER, METRIC_CHART } from '../constants';
|
||||||
import reportMixin from '../../../../../mixins/reportMixin';
|
import reportMixin from '../../../../../mixins/reportMixin';
|
||||||
|
|
||||||
const REPORTS_KEYS = {
|
const REPORTS_KEYS = {
|
||||||
|
@ -137,16 +139,38 @@ export default {
|
||||||
}
|
}
|
||||||
return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy');
|
return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy');
|
||||||
});
|
});
|
||||||
const data = this.accountReport.data.map(element => element.value);
|
|
||||||
|
const datasets = METRIC_CHART[
|
||||||
|
this.metrics[this.currentSelection].KEY
|
||||||
|
].datasets.map(dataset => {
|
||||||
|
switch (dataset.type) {
|
||||||
|
case 'bar':
|
||||||
|
return {
|
||||||
|
...dataset,
|
||||||
|
yAxisID: 'y-left',
|
||||||
|
label: this.metrics[this.currentSelection].NAME,
|
||||||
|
data: this.accountReport.data.map(element => element.value),
|
||||||
|
};
|
||||||
|
case 'line':
|
||||||
|
return {
|
||||||
|
...dataset,
|
||||||
|
yAxisID: 'y-right',
|
||||||
|
label: this.metrics[0].NAME,
|
||||||
|
data: this.accountReport.data.map(element => element.count),
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return dataset;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
labels,
|
labels,
|
||||||
datasets: [
|
datasets,
|
||||||
{
|
};
|
||||||
label: this.metrics[this.currentSelection].NAME,
|
},
|
||||||
backgroundColor: '#1f93ff',
|
chartOptions() {
|
||||||
data,
|
return {
|
||||||
},
|
scales: METRIC_CHART[this.metrics[this.currentSelection].KEY].scales,
|
||||||
],
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
metrics() {
|
metrics() {
|
||||||
|
@ -168,6 +192,7 @@ export default {
|
||||||
NAME: this.$t(`REPORT.METRICS.${key}.NAME`),
|
NAME: this.$t(`REPORT.METRICS.${key}.NAME`),
|
||||||
KEY: REPORTS_KEYS[key],
|
KEY: REPORTS_KEYS[key],
|
||||||
DESC: this.$t(`REPORT.METRICS.${key}.DESC`),
|
DESC: this.$t(`REPORT.METRICS.${key}.DESC`),
|
||||||
|
INFO_TEXT: this.$t(`REPORT.METRICS.${key}.INFO_TEXT`),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,3 +4,142 @@ export const GROUP_BY_FILTER = {
|
||||||
3: { id: 3, period: 'month' },
|
3: { id: 3, period: 'month' },
|
||||||
4: { id: 4, period: 'year' },
|
4: { id: 4, period: 'year' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CHART_FONT_FAMILY =
|
||||||
|
'-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
||||||
|
|
||||||
|
export const DEFAULT_LINE_CHART = {
|
||||||
|
type: 'line',
|
||||||
|
fill: false,
|
||||||
|
borderColor: '#779BBB',
|
||||||
|
pointBackgroundColor: '#779BBB',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_BAR_CHART = {
|
||||||
|
type: 'bar',
|
||||||
|
backgroundColor: 'rgb(31, 147, 255, 0.5)',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_CHART = {
|
||||||
|
datasets: [DEFAULT_BAR_CHART],
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
fontFamily: CHART_FONT_FAMILY,
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
id: 'y-left',
|
||||||
|
type: 'linear',
|
||||||
|
position: 'left',
|
||||||
|
ticks: {
|
||||||
|
fontFamily: CHART_FONT_FAMILY,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const METRIC_CHART = {
|
||||||
|
conversations_count: DEFAULT_CHART,
|
||||||
|
incoming_messages_count: DEFAULT_CHART,
|
||||||
|
outgoing_messages_count: DEFAULT_CHART,
|
||||||
|
avg_first_response_time: {
|
||||||
|
datasets: [DEFAULT_BAR_CHART, DEFAULT_LINE_CHART],
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
fontFamily: CHART_FONT_FAMILY,
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
id: 'y-left',
|
||||||
|
type: 'linear',
|
||||||
|
position: 'left',
|
||||||
|
ticks: {
|
||||||
|
fontFamily: CHART_FONT_FAMILY,
|
||||||
|
beginAtZero: true,
|
||||||
|
precision: 2,
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'y-right',
|
||||||
|
type: 'linear',
|
||||||
|
position: 'right',
|
||||||
|
ticks: {
|
||||||
|
fontFamily: CHART_FONT_FAMILY,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
avg_resolution_time: {
|
||||||
|
datasets: [DEFAULT_BAR_CHART, DEFAULT_LINE_CHART],
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
fontFamily: CHART_FONT_FAMILY,
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
id: 'y-left',
|
||||||
|
type: 'linear',
|
||||||
|
position: 'left',
|
||||||
|
ticks: {
|
||||||
|
fontFamily: CHART_FONT_FAMILY,
|
||||||
|
beginAtZero: true,
|
||||||
|
precision: 2,
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'y-right',
|
||||||
|
type: 'linear',
|
||||||
|
position: 'right',
|
||||||
|
ticks: {
|
||||||
|
fontFamily: CHART_FONT_FAMILY,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolutions_count: DEFAULT_CHART,
|
||||||
|
};
|
||||||
|
|
|
@ -52,4 +52,8 @@ export const actions = {
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addNotification({ commit }, data) {
|
||||||
|
commit(types.ADD_NOTIFICATION, data);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -45,4 +45,14 @@ export const mutations = {
|
||||||
Vue.set($state.records[item.id], 'read_at', true);
|
Vue.set($state.records[item.id], 'read_at', true);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[types.ADD_NOTIFICATION]($state, data) {
|
||||||
|
const { notification, unread_count: unreadCount, count } = data;
|
||||||
|
Vue.set($state.records, notification.id, {
|
||||||
|
...($state.records[notification.id] || {}),
|
||||||
|
...notification,
|
||||||
|
});
|
||||||
|
Vue.set($state.meta, 'unreadCount', unreadCount);
|
||||||
|
Vue.set($state.meta, 'count', count);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -90,4 +90,12 @@ describe('#actions', () => {
|
||||||
await expect(actions.readAll({ commit })).rejects.toThrow(Error);
|
await expect(actions.readAll({ commit })).rejects.toThrow(Error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('#addNotification', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
await actions.addNotification({ commit }, { data: 1 });
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.ADD_NOTIFICATION, { data: 1 }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -93,4 +93,29 @@ describe('#mutations', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#ADD_NOTIFICATION', () => {
|
||||||
|
it('add notification', () => {
|
||||||
|
const state = {
|
||||||
|
meta: { unreadCount: 4, count: 231 },
|
||||||
|
records: {
|
||||||
|
1: { id: 1, primary_actor_id: 1 },
|
||||||
|
2: { id: 2, primary_actor_id: 2 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const data = {
|
||||||
|
notification: { id: 3, primary_actor_id: 3 },
|
||||||
|
unread_count: 5,
|
||||||
|
count: 232,
|
||||||
|
};
|
||||||
|
mutations[types.ADD_NOTIFICATION](state, data);
|
||||||
|
expect(state.records).toEqual({
|
||||||
|
1: { id: 1, primary_actor_id: 1 },
|
||||||
|
2: { id: 2, primary_actor_id: 2 },
|
||||||
|
3: { id: 3, primary_actor_id: 3 },
|
||||||
|
});
|
||||||
|
expect(state.meta.unreadCount).toEqual(5);
|
||||||
|
expect(state.meta.count).toEqual(232);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -116,6 +116,7 @@ export default {
|
||||||
SET_NOTIFICATIONS_UNREAD_COUNT: 'SET_NOTIFICATIONS_UNREAD_COUNT',
|
SET_NOTIFICATIONS_UNREAD_COUNT: 'SET_NOTIFICATIONS_UNREAD_COUNT',
|
||||||
SET_NOTIFICATIONS_UI_FLAG: 'SET_NOTIFICATIONS_UI_FLAG',
|
SET_NOTIFICATIONS_UI_FLAG: 'SET_NOTIFICATIONS_UI_FLAG',
|
||||||
UPDATE_NOTIFICATION: 'UPDATE_NOTIFICATION',
|
UPDATE_NOTIFICATION: 'UPDATE_NOTIFICATION',
|
||||||
|
ADD_NOTIFICATION: 'ADD_NOTIFICATION',
|
||||||
UPDATE_ALL_NOTIFICATIONS: 'UPDATE_ALL_NOTIFICATIONS',
|
UPDATE_ALL_NOTIFICATIONS: 'UPDATE_ALL_NOTIFICATIONS',
|
||||||
SET_NOTIFICATIONS_ITEM: 'SET_NOTIFICATIONS_ITEM',
|
SET_NOTIFICATIONS_ITEM: 'SET_NOTIFICATIONS_ITEM',
|
||||||
SET_NOTIFICATIONS: 'SET_NOTIFICATIONS',
|
SET_NOTIFICATIONS: 'SET_NOTIFICATIONS',
|
||||||
|
|
|
@ -31,6 +31,14 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
||||||
IFrameHelper.events.toggleBubble(state);
|
IFrameHelper.events.toggleBubble(state);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
popoutChatWindow() {
|
||||||
|
IFrameHelper.events.popoutChatWindow({
|
||||||
|
baseUrl: window.$chatwoot.baseUrl,
|
||||||
|
websiteToken: window.$chatwoot.websiteToken,
|
||||||
|
locale: window.$chatwoot.locale,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
setUser(identifier, user) {
|
setUser(identifier, user) {
|
||||||
if (typeof identifier !== 'string' && typeof identifier !== 'number') {
|
if (typeof identifier !== 'string' && typeof identifier !== 'number') {
|
||||||
throw new Error('Identifier should be a string or a number');
|
throw new Error('Identifier should be a string or a number');
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
initOnEvents,
|
initOnEvents,
|
||||||
} from 'shared/helpers/AudioNotificationHelper';
|
} from 'shared/helpers/AudioNotificationHelper';
|
||||||
import { isFlatWidgetStyle } from './settingsHelper';
|
import { isFlatWidgetStyle } from './settingsHelper';
|
||||||
|
import { popoutChatWindow } from '../widget/helpers/popoutHelper';
|
||||||
|
|
||||||
export const IFrameHelper = {
|
export const IFrameHelper = {
|
||||||
getUrl({ baseUrl, websiteToken }) {
|
getUrl({ baseUrl, websiteToken }) {
|
||||||
|
@ -190,6 +191,12 @@ export const IFrameHelper = {
|
||||||
onBubbleClick(bubbleState);
|
onBubbleClick(bubbleState);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
popoutChatWindow: ({ baseUrl, websiteToken, locale }) => {
|
||||||
|
const cwCookie = Cookies.get('cw_conversation');
|
||||||
|
window.$chatwoot.toggle('close');
|
||||||
|
popoutChatWindow(baseUrl, websiteToken, locale, cwCookie);
|
||||||
|
},
|
||||||
|
|
||||||
closeWindow: () => {
|
closeWindow: () => {
|
||||||
onBubbleClick({ toggleValue: false });
|
onBubbleClick({ toggleValue: false });
|
||||||
removeUnreadClass();
|
removeUnreadClass();
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
|
import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
|
||||||
import { buildPopoutURL } from '../helpers/urlParamsHelper';
|
import { popoutChatWindow } from '../helpers/popoutHelper';
|
||||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -69,19 +69,12 @@ export default {
|
||||||
chatwootWebChannel: { websiteToken },
|
chatwootWebChannel: { websiteToken },
|
||||||
authToken,
|
authToken,
|
||||||
} = window;
|
} = window;
|
||||||
|
popoutChatWindow(
|
||||||
const popoutWindowURL = buildPopoutURL({
|
|
||||||
origin,
|
origin,
|
||||||
websiteToken,
|
websiteToken,
|
||||||
locale: this.$root.$i18n.locale,
|
this.$root.$i18n.locale,
|
||||||
conversationCookie: authToken,
|
authToken
|
||||||
});
|
|
||||||
const popoutWindow = window.open(
|
|
||||||
popoutWindowURL,
|
|
||||||
`webwidget_session_${websiteToken}`,
|
|
||||||
'resizable=off,width=400,height=600'
|
|
||||||
);
|
);
|
||||||
popoutWindow.focus();
|
|
||||||
},
|
},
|
||||||
closeWindow() {
|
closeWindow() {
|
||||||
if (IFrameHelper.isIFrame()) {
|
if (IFrameHelper.isIFrame()) {
|
||||||
|
|
26
app/javascript/widget/helpers/popoutHelper.js
Normal file
26
app/javascript/widget/helpers/popoutHelper.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { buildPopoutURL } from './urlParamsHelper';
|
||||||
|
|
||||||
|
export const popoutChatWindow = (
|
||||||
|
origin,
|
||||||
|
websiteToken,
|
||||||
|
locale,
|
||||||
|
conversationCookie
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const windowUrl = buildPopoutURL({
|
||||||
|
origin,
|
||||||
|
websiteToken,
|
||||||
|
locale,
|
||||||
|
conversationCookie,
|
||||||
|
});
|
||||||
|
const popoutWindow = window.open(
|
||||||
|
windowUrl,
|
||||||
|
`webwidget_session_${websiteToken}`,
|
||||||
|
'resizable=off,width=400,height=600'
|
||||||
|
);
|
||||||
|
popoutWindow.focus();
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
};
|
|
@ -58,6 +58,7 @@ export const actions = {
|
||||||
if (identifier_hash) {
|
if (identifier_hash) {
|
||||||
dispatch('conversation/clearConversations', {}, { root: true });
|
dispatch('conversation/clearConversations', {}, { root: true });
|
||||||
dispatch('conversation/fetchOldConversations', {}, { root: true });
|
dispatch('conversation/fetchOldConversations', {}, { root: true });
|
||||||
|
dispatch('conversationAttributes/getAttributes', {}, { root: true });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const {
|
const {
|
||||||
|
|
|
@ -21,6 +21,7 @@ describe('#actions', () => {
|
||||||
['get'],
|
['get'],
|
||||||
['conversation/clearConversations', {}, { root: true }],
|
['conversation/clearConversations', {}, { root: true }],
|
||||||
['conversation/fetchOldConversations', {}, { root: true }],
|
['conversation/fetchOldConversations', {}, { root: true }],
|
||||||
|
['conversationAttributes/getAttributes', {}, { root: true }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,6 @@ class ContactAvatarJob < ApplicationJob
|
||||||
)
|
)
|
||||||
contact.avatar.attach(io: avatar_file, filename: avatar_file.original_filename, content_type: avatar_file.content_type)
|
contact.avatar.attach(io: avatar_file, filename: avatar_file.original_filename, content_type: avatar_file.content_type)
|
||||||
rescue Down::Error => e
|
rescue Down::Error => e
|
||||||
Rails.logger.info "Exception: invalid avatar url #{avatar_url} : #{e.message}"
|
Rails.logger.error "Exception: invalid avatar url #{avatar_url} : #{e.message}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,7 @@ class ContactIpLookupJob < ApplicationJob
|
||||||
|
|
||||||
update_contact_location_from_ip(contact)
|
update_contact_location_from_ip(contact)
|
||||||
rescue Errno::ETIMEDOUT => e
|
rescue Errno::ETIMEDOUT => e
|
||||||
Rails.logger.info "Exception: ip resolution failed : #{e.message}"
|
Rails.logger.warn "Exception: ip resolution failed : #{e.message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -2,9 +2,9 @@ class ActionCableListener < BaseListener
|
||||||
include Events::Types
|
include Events::Types
|
||||||
|
|
||||||
def notification_created(event)
|
def notification_created(event)
|
||||||
notification, account = extract_notification_and_account(event)
|
notification, account, unread_count, count = extract_notification_and_account(event)
|
||||||
tokens = [event.data[:notification].user.pubsub_token]
|
tokens = [event.data[:notification].user.pubsub_token]
|
||||||
broadcast(account, tokens, NOTIFICATION_CREATED, notification.push_event_data)
|
broadcast(account, tokens, NOTIFICATION_CREATED, { notification: notification.push_event_data, unread_count: unread_count, count: count })
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_created(event)
|
def message_created(event)
|
||||||
|
|
|
@ -8,7 +8,9 @@ class BaseListener
|
||||||
|
|
||||||
def extract_notification_and_account(event)
|
def extract_notification_and_account(event)
|
||||||
notification = event.data[:notification]
|
notification = event.data[:notification]
|
||||||
[notification, notification.account]
|
unread_count = notification.user.notifications_meta[:unread_count]
|
||||||
|
count = notification.user.notifications_meta[:count]
|
||||||
|
[notification, notification.account, unread_count, count]
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_message_and_account(event)
|
def extract_message_and_account(event)
|
||||||
|
@ -20,4 +22,12 @@ class BaseListener
|
||||||
contact = event.data[:contact]
|
contact = event.data[:contact]
|
||||||
[contact, contact.account]
|
[contact, contact.account]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def extract_changed_attributes(event)
|
||||||
|
changed_attributes = event.data[:changed_attributes]
|
||||||
|
|
||||||
|
return if changed_attributes.blank?
|
||||||
|
|
||||||
|
changed_attributes.map { |k, v| { k => { previous_value: v[0], current_value: v[1] } } }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,23 +2,34 @@ class WebhookListener < BaseListener
|
||||||
# FIXME: deprecate the opened and resolved events in future in favor of status changed event.
|
# FIXME: deprecate the opened and resolved events in future in favor of status changed event.
|
||||||
def conversation_resolved(event)
|
def conversation_resolved(event)
|
||||||
conversation = extract_conversation_and_account(event)[0]
|
conversation = extract_conversation_and_account(event)[0]
|
||||||
|
changed_attributes = extract_changed_attributes(event)
|
||||||
inbox = conversation.inbox
|
inbox = conversation.inbox
|
||||||
payload = conversation.webhook_data.merge(event: __method__.to_s)
|
payload = conversation.webhook_data.merge(event: __method__.to_s, changed_attributes: changed_attributes)
|
||||||
deliver_webhook_payloads(payload, inbox)
|
deliver_webhook_payloads(payload, inbox)
|
||||||
end
|
end
|
||||||
|
|
||||||
# FIXME: deprecate the opened and resolved events in future in favor of status changed event.
|
# FIXME: deprecate the opened and resolved events in future in favor of status changed event.
|
||||||
def conversation_opened(event)
|
def conversation_opened(event)
|
||||||
conversation = extract_conversation_and_account(event)[0]
|
conversation = extract_conversation_and_account(event)[0]
|
||||||
|
changed_attributes = extract_changed_attributes(event)
|
||||||
inbox = conversation.inbox
|
inbox = conversation.inbox
|
||||||
payload = conversation.webhook_data.merge(event: __method__.to_s)
|
payload = conversation.webhook_data.merge(event: __method__.to_s, changed_attributes: changed_attributes)
|
||||||
deliver_webhook_payloads(payload, inbox)
|
deliver_webhook_payloads(payload, inbox)
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation_status_changed(event)
|
def conversation_status_changed(event)
|
||||||
conversation = extract_conversation_and_account(event)[0]
|
conversation = extract_conversation_and_account(event)[0]
|
||||||
|
changed_attributes = extract_changed_attributes(event)
|
||||||
inbox = conversation.inbox
|
inbox = conversation.inbox
|
||||||
payload = conversation.webhook_data.merge(event: __method__.to_s)
|
payload = conversation.webhook_data.merge(event: __method__.to_s, changed_attributes: changed_attributes)
|
||||||
|
deliver_webhook_payloads(payload, inbox)
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversation_updated(event)
|
||||||
|
conversation = extract_conversation_and_account(event)[0]
|
||||||
|
changed_attributes = extract_changed_attributes(event)
|
||||||
|
inbox = conversation.inbox
|
||||||
|
payload = conversation.webhook_data.merge(event: __method__.to_s, changed_attributes: changed_attributes)
|
||||||
deliver_webhook_payloads(payload, inbox)
|
deliver_webhook_payloads(payload, inbox)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,8 @@ class ApplicationMailer < ActionMailer::Base
|
||||||
private
|
private
|
||||||
|
|
||||||
def handle_smtp_exceptions(message)
|
def handle_smtp_exceptions(message)
|
||||||
Rails.logger.info 'Failed to send Email'
|
Rails.logger.warn 'Failed to send Email'
|
||||||
Rails.logger.info "Exception: #{message}"
|
Rails.logger.error "Exception: #{message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_mail_with_liquid(*args)
|
def send_mail_with_liquid(*args)
|
||||||
|
|
|
@ -72,6 +72,7 @@ class Account < ApplicationRecord
|
||||||
has_many :sms_channels, dependent: :destroy_async, class_name: '::Channel::Sms'
|
has_many :sms_channels, dependent: :destroy_async, class_name: '::Channel::Sms'
|
||||||
has_many :working_hours, dependent: :destroy_async
|
has_many :working_hours, dependent: :destroy_async
|
||||||
has_many :automation_rules, dependent: :destroy
|
has_many :automation_rules, dependent: :destroy
|
||||||
|
has_many :notifications, dependent: :destroy
|
||||||
|
|
||||||
has_flags ACCOUNT_SETTINGS_FLAGS.merge(column: 'settings_flags').merge(DEFAULT_QUERY_SETTING)
|
has_flags ACCOUNT_SETTINGS_FLAGS.merge(column: 'settings_flags').merge(DEFAULT_QUERY_SETTING)
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
#
|
#
|
||||||
class AutomationRule < ApplicationRecord
|
class AutomationRule < ApplicationRecord
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
has_many_attached :files
|
||||||
|
|
||||||
validate :json_conditions_format
|
validate :json_conditions_format
|
||||||
validate :json_actions_format
|
validate :json_actions_format
|
||||||
|
@ -26,8 +27,8 @@ class AutomationRule < ApplicationRecord
|
||||||
|
|
||||||
scope :active, -> { where(active: true) }
|
scope :active, -> { where(active: true) }
|
||||||
|
|
||||||
CONDITIONS_ATTRS = %w[country_code status browser_language assignee_id team_id referer].freeze
|
CONDITIONS_ATTRS = %w[email country_code status message_type browser_language assignee_id team_id referer city company].freeze
|
||||||
ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_best_agents].freeze
|
ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_best_agents send_attachments].freeze
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
@ -35,13 +36,17 @@ class AutomationRule < ApplicationRecord
|
||||||
return if conditions.nil?
|
return if conditions.nil?
|
||||||
|
|
||||||
attributes = conditions.map { |obj, _| obj['attribute_key'] }
|
attributes = conditions.map { |obj, _| obj['attribute_key'] }
|
||||||
(attributes - CONDITIONS_ATTRS).blank?
|
conditions = attributes - CONDITIONS_ATTRS
|
||||||
|
conditions -= account.custom_attribute_definitions.pluck(:attribute_key)
|
||||||
|
errors.add(:conditions, "Automation conditions #{conditions.join(',')} not supported.") if conditions.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
def json_actions_format
|
def json_actions_format
|
||||||
return if actions.nil?
|
return if actions.nil?
|
||||||
|
|
||||||
attributes = actions.map { |obj, _| obj['attribute_key'] }
|
attributes = actions.map { |obj, _| obj['attribute_key'] }
|
||||||
(attributes - ACTIONS_ATTRS).blank?
|
actions = attributes - ACTIONS_ATTRS
|
||||||
|
|
||||||
|
errors.add(:actions, "Automation actions #{actions.join(',')} not supported.") if actions.any?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -45,7 +45,7 @@ class Channel::FacebookPage < ApplicationRecord
|
||||||
source_id: instagram_id
|
source_id: instagram_id
|
||||||
)
|
)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.info e
|
Rails.logger.error e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ class Channel::TwitterProfile < ApplicationRecord
|
||||||
source_id: profile_id
|
source_id: profile_id
|
||||||
)
|
)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.info e
|
Rails.logger.error e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -62,6 +62,6 @@ class Channel::TwitterProfile < ApplicationRecord
|
||||||
unsubscribe_response = twitter_client.remove_subscription(user_id: profile_id)
|
unsubscribe_response = twitter_client.remove_subscription(user_id: profile_id)
|
||||||
Rails.logger.info "TWITTER_UNSUBSCRIBE: #{unsubscribe_response.body}"
|
Rails.logger.info "TWITTER_UNSUBSCRIBE: #{unsubscribe_response.body}"
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.info e
|
Rails.logger.error e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -86,7 +86,7 @@ class Channel::WebWidget < ApplicationRecord
|
||||||
)
|
)
|
||||||
contact_inbox
|
contact_inbox
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.info e
|
Rails.logger.error e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,8 @@ module ActivityMessageHandler
|
||||||
end
|
end
|
||||||
|
|
||||||
def status_change_activity(user_name)
|
def status_change_activity(user_name)
|
||||||
|
return send_automation_activity if Current.executed_by.present?
|
||||||
|
|
||||||
create_status_change_message(user_name)
|
create_status_change_message(user_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -29,6 +31,11 @@ module ActivityMessageHandler
|
||||||
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_automation_activity
|
||||||
|
content = I18n.t("conversations.activity.status.#{status}", user_name: 'Automation System')
|
||||||
|
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||||
|
end
|
||||||
|
|
||||||
def create_label_added(user_name, labels = [])
|
def create_label_added(user_name, labels = [])
|
||||||
return unless labels.size.positive?
|
return unless labels.size.positive?
|
||||||
|
|
||||||
|
|
|
@ -12,18 +12,19 @@ module AssignmentHandler
|
||||||
def ensure_assignee_is_from_team
|
def ensure_assignee_is_from_team
|
||||||
return unless team_id_changed?
|
return unless team_id_changed?
|
||||||
|
|
||||||
ensure_current_assignee_team
|
validate_current_assignee_team
|
||||||
self.assignee_id ||= find_team_assignee_id_for_inbox if team&.allow_auto_assign.present?
|
self.assignee ||= find_assignee_from_team
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_current_assignee_team
|
def validate_current_assignee_team
|
||||||
self.assignee_id = nil if team&.members&.exclude?(assignee)
|
self.assignee_id = nil if team&.members&.exclude?(assignee)
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_team_assignee_id_for_inbox
|
def find_assignee_from_team
|
||||||
members = inbox.members.ids & team.members.ids
|
return if team&.allow_auto_assign.blank?
|
||||||
# TODO: User round robin to determine the next agent instead of using sample
|
|
||||||
members.sample
|
team_members = inbox.members.ids & team.members.ids
|
||||||
|
::RoundRobin::AssignmentService.new(conversation: self, allowed_member_ids: team_members).find_assignee
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_assignment_change
|
def notify_assignment_change
|
||||||
|
|
|
@ -188,7 +188,7 @@ class Conversation < ApplicationRecord
|
||||||
return unless previous_changes.keys.present? && (previous_changes.keys & %w[team_id assignee_id status snoozed_until
|
return unless previous_changes.keys.present? && (previous_changes.keys & %w[team_id assignee_id status snoozed_until
|
||||||
custom_attributes]).present?
|
custom_attributes]).present?
|
||||||
|
|
||||||
dispatcher_dispatch(CONVERSATION_UPDATED)
|
dispatcher_dispatch(CONVERSATION_UPDATED, previous_changes)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self_assign?(assignee_id)
|
def self_assign?(assignee_id)
|
||||||
|
@ -207,14 +207,15 @@ class Conversation < ApplicationRecord
|
||||||
CONVERSATION_READ => -> { saved_change_to_contact_last_seen_at? },
|
CONVERSATION_READ => -> { saved_change_to_contact_last_seen_at? },
|
||||||
CONVERSATION_CONTACT_CHANGED => -> { saved_change_to_contact_id? }
|
CONVERSATION_CONTACT_CHANGED => -> { saved_change_to_contact_id? }
|
||||||
}.each do |event, condition|
|
}.each do |event, condition|
|
||||||
condition.call && dispatcher_dispatch(event)
|
condition.call && dispatcher_dispatch(event, status_change)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def dispatcher_dispatch(event_name)
|
def dispatcher_dispatch(event_name, changed_attributes = nil)
|
||||||
return if Current.executed_by.present? && Current.executed_by.instance_of?(AutomationRule)
|
return if Current.executed_by.present? && Current.executed_by.instance_of?(AutomationRule)
|
||||||
|
|
||||||
Rails.configuration.dispatcher.dispatch(event_name, Time.zone.now, conversation: self, notifiable_assignee_change: notifiable_assignee_change?)
|
Rails.configuration.dispatcher.dispatch(event_name, Time.zone.now, conversation: self, notifiable_assignee_change: notifiable_assignee_change?,
|
||||||
|
changed_attributes: changed_attributes)
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation_status_changed_to_open?
|
def conversation_status_changed_to_open?
|
||||||
|
|
|
@ -52,15 +52,24 @@ class Notification < ApplicationRecord
|
||||||
notification_type: notification_type,
|
notification_type: notification_type,
|
||||||
primary_actor_type: primary_actor_type,
|
primary_actor_type: primary_actor_type,
|
||||||
primary_actor_id: primary_actor_id,
|
primary_actor_id: primary_actor_id,
|
||||||
primary_actor: primary_actor.push_event_data,
|
primary_actor: primary_actor_data,
|
||||||
read_at: read_at,
|
read_at: read_at,
|
||||||
secondary_actor: secondary_actor&.push_event_data,
|
secondary_actor: secondary_actor&.push_event_data,
|
||||||
user: user&.push_event_data,
|
user: user&.push_event_data,
|
||||||
created_at: created_at,
|
created_at: created_at.to_i,
|
||||||
account_id: account_id
|
account_id: account_id,
|
||||||
|
push_message_title: push_message_title
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def primary_actor_data
|
||||||
|
if %w[assigned_conversation_new_message conversation_mention].include? notification_type
|
||||||
|
primary_actor.conversation.push_event_data
|
||||||
|
else
|
||||||
|
primary_actor.push_event_data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def fcm_push_data
|
def fcm_push_data
|
||||||
{
|
{
|
||||||
id: id,
|
id: id,
|
||||||
|
|
|
@ -187,4 +187,11 @@ class User < ApplicationRecord
|
||||||
def will_save_change_to_email?
|
def will_save_change_to_email?
|
||||||
mutations_from_database.changed?('email')
|
mutations_from_database.changed?('email')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def notifications_meta
|
||||||
|
{
|
||||||
|
unread_count: notifications.where(read_at: nil).count,
|
||||||
|
count: notifications.count
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,28 +21,43 @@ class AutomationRules::ActionService
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def send_email_transcript(email)
|
def send_attachments(_file_params)
|
||||||
ConversationReplyMailer.with(account: conversation.account).conversation_transcript(@conversation, email)&.deliver_later
|
return if @rule.event_name == 'message_created'
|
||||||
|
|
||||||
|
blobs = @rule.files.map { |file, _| file.blob }
|
||||||
|
params = { content: nil, private: false, attachments: blobs }
|
||||||
|
mb = Messages::MessageBuilder.new(nil, @conversation, params)
|
||||||
|
mb.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_email_transcript(emails)
|
||||||
|
emails.each do |email|
|
||||||
|
ConversationReplyMailer.with(account: @conversation.account).conversation_transcript(@conversation, email)&.deliver_later
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def mute_conversation(_params)
|
def mute_conversation(_params)
|
||||||
@conversation.mute!
|
@conversation.mute!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def snooze_conversation(_params)
|
||||||
|
@conversation.ensure_snooze_until_reset
|
||||||
|
end
|
||||||
|
|
||||||
def change_status(status)
|
def change_status(status)
|
||||||
@conversation.update!(status: status[0])
|
@conversation.update!(status: status[0])
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_webhook_events(webhook_url)
|
def send_webhook_event(webhook_url)
|
||||||
payload = @conversation.webhook_data.merge(event: "automation_event: #{@rule.event_name}")
|
payload = @conversation.webhook_data.merge(event: "automation_event: #{@rule.event_name}")
|
||||||
WebhookJob.perform_later(webhook_url, payload)
|
WebhookJob.perform_later(webhook_url[0], payload)
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_message(message)
|
def send_message(message)
|
||||||
return if @rule.event_name == 'message_created'
|
return if @rule.event_name == 'message_created'
|
||||||
|
|
||||||
params = { content: message[0], private: false }
|
params = { content: message[0], private: false }
|
||||||
mb = Messages::MessageBuilder.new(@administrator, @conversation, params)
|
mb = Messages::MessageBuilder.new(nil, @conversation, params)
|
||||||
mb.perform
|
mb.perform
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -79,10 +94,6 @@ class AutomationRules::ActionService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def administrator
|
|
||||||
@administrator ||= @account.administrators.first
|
|
||||||
end
|
|
||||||
|
|
||||||
def agent_belongs_to_account?(agent_ids)
|
def agent_belongs_to_account?(agent_ids)
|
||||||
@account.agents.pluck(:id).include?(agent_ids[0])
|
@account.agents.pluck(:id).include?(agent_ids[0])
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,20 +1,33 @@
|
||||||
require 'json'
|
require 'json'
|
||||||
|
|
||||||
class AutomationRules::ConditionsFilterService < FilterService
|
class AutomationRules::ConditionsFilterService < FilterService
|
||||||
|
ATTRIBUTE_MODEL = 'contact_attribute'.freeze
|
||||||
|
|
||||||
def initialize(rule, conversation = nil)
|
def initialize(rule, conversation = nil)
|
||||||
super([], nil)
|
super([], nil)
|
||||||
@rule = rule
|
@rule = rule
|
||||||
@conversation = conversation
|
@conversation = conversation
|
||||||
|
@account = conversation.account
|
||||||
file = File.read('./lib/filters/filter_keys.json')
|
file = File.read('./lib/filters/filter_keys.json')
|
||||||
@filters = JSON.parse(file)
|
@filters = JSON.parse(file)
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
conversation_filters = @filters['conversations']
|
conversation_filters = @filters['conversations']
|
||||||
|
contact_filters = @filters['contacts']
|
||||||
|
|
||||||
@rule.conditions.each_with_index do |query_hash, current_index|
|
@rule.conditions.each_with_index do |query_hash, current_index|
|
||||||
current_filter = conversation_filters[query_hash['attribute_key']]
|
conversation_filter = conversation_filters[query_hash['attribute_key']]
|
||||||
@query_string += conversation_query_string(current_filter, query_hash.with_indifferent_access, current_index)
|
contact_filter = contact_filters[query_hash['attribute_key']]
|
||||||
|
|
||||||
|
if conversation_filter
|
||||||
|
@query_string += conversation_query_string('conversations', conversation_filter, query_hash.with_indifferent_access, current_index)
|
||||||
|
elsif contact_filter
|
||||||
|
@query_string += conversation_query_string('contacts', contact_filter, query_hash.with_indifferent_access, current_index)
|
||||||
|
elsif custom_attribute(query_hash['attribute_key'], @account)
|
||||||
|
# send table name according to attribute key right now we are supporting contact based custom attribute filter
|
||||||
|
@query_string += custom_attribute_query(query_hash.with_indifferent_access, 'contacts', current_index)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
records = base_relation.where(@query_string, @filter_values.with_indifferent_access)
|
records = base_relation.where(@query_string, @filter_values.with_indifferent_access)
|
||||||
|
@ -57,25 +70,26 @@ class AutomationRules::ConditionsFilterService < FilterService
|
||||||
records.any?
|
records.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation_query_string(current_filter, query_hash, current_index)
|
def conversation_query_string(table_name, current_filter, query_hash, current_index)
|
||||||
attribute_key = query_hash['attribute_key']
|
attribute_key = query_hash['attribute_key']
|
||||||
query_operator = query_hash['query_operator']
|
query_operator = query_hash['query_operator']
|
||||||
|
|
||||||
filter_operator_value = filter_operation(query_hash, current_index)
|
filter_operator_value = filter_operation(query_hash, current_index)
|
||||||
|
|
||||||
case current_filter['attribute_type']
|
case current_filter['attribute_type']
|
||||||
when 'additional_attributes'
|
when 'additional_attributes'
|
||||||
" conversations.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} "
|
" #{table_name}.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} "
|
||||||
when 'standard'
|
when 'standard'
|
||||||
if attribute_key == 'labels'
|
if attribute_key == 'labels'
|
||||||
" tags.id #{filter_operator_value} #{query_operator} "
|
" tags.id #{filter_operator_value} #{query_operator} "
|
||||||
else
|
else
|
||||||
" conversations.#{attribute_key} #{filter_operator_value} #{query_operator} "
|
" #{table_name}.#{attribute_key} #{filter_operator_value} #{query_operator} "
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def base_relation
|
def base_relation
|
||||||
Conversation.where(id: @conversation.id)
|
Conversation.where(id: @conversation.id).joins('LEFT OUTER JOIN contacts on conversations.contact_id = contacts.id')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,7 +26,7 @@ class Contacts::FilterService < FilterService
|
||||||
query_operator = query_hash[:query_operator]
|
query_operator = query_hash[:query_operator]
|
||||||
filter_operator_value = filter_operation(query_hash, current_index)
|
filter_operator_value = filter_operation(query_hash, current_index)
|
||||||
|
|
||||||
return custom_attribute_query(query_hash, current_index) if current_filter.nil?
|
return custom_attribute_query(query_hash, 'contacts', current_index) if current_filter.nil?
|
||||||
|
|
||||||
case current_filter['attribute_type']
|
case current_filter['attribute_type']
|
||||||
when 'additional_attributes'
|
when 'additional_attributes'
|
||||||
|
@ -64,18 +64,4 @@ class Contacts::FilterService < FilterService
|
||||||
|
|
||||||
"!= :value_#{current_index}"
|
"!= :value_#{current_index}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def custom_attribute_query(query_hash, current_index)
|
|
||||||
attribute_key = query_hash[:attribute_key]
|
|
||||||
query_operator = query_hash[:query_operator]
|
|
||||||
attribute_type = custom_attribute(attribute_key).try(:attribute_display_type)
|
|
||||||
filter_operator_value = filter_operation(query_hash, current_index)
|
|
||||||
attribute_data_type = self.class::ATTRIBUTE_TYPES[attribute_type]
|
|
||||||
|
|
||||||
if custom_attribute(attribute_key)
|
|
||||||
" LOWER(contacts.custom_attributes ->> '#{attribute_key}')::#{attribute_data_type} #{filter_operator_value} #{query_operator} "
|
|
||||||
else
|
|
||||||
' '
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,7 +32,7 @@ class Conversations::FilterService < FilterService
|
||||||
query_operator = query_hash[:query_operator]
|
query_operator = query_hash[:query_operator]
|
||||||
filter_operator_value = filter_operation(query_hash, current_index)
|
filter_operator_value = filter_operation(query_hash, current_index)
|
||||||
|
|
||||||
return custom_attribute_query(query_hash, current_index) if current_filter.nil?
|
return custom_attribute_query(query_hash, 'conversations', current_index) if current_filter.nil?
|
||||||
|
|
||||||
case current_filter['attribute_type']
|
case current_filter['attribute_type']
|
||||||
when 'additional_attributes'
|
when 'additional_attributes'
|
||||||
|
@ -62,20 +62,4 @@ class Conversations::FilterService < FilterService
|
||||||
)
|
)
|
||||||
@conversations.latest.page(current_page)
|
@conversations.latest.page(current_page)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def custom_attribute_query(query_hash, current_index)
|
|
||||||
attribute_key = query_hash[:attribute_key]
|
|
||||||
query_operator = query_hash[:query_operator]
|
|
||||||
attribute_type = custom_attribute(attribute_key).try(:attribute_display_type)
|
|
||||||
filter_operator_value = filter_operation(query_hash, current_index)
|
|
||||||
attribute_data_type = self.class::ATTRIBUTE_TYPES[attribute_type]
|
|
||||||
|
|
||||||
if custom_attribute(attribute_key)
|
|
||||||
" LOWER(conversations.custom_attributes ->> '#{attribute_key}')::#{attribute_data_type} #{filter_operator_value} #{query_operator} "
|
|
||||||
else
|
|
||||||
' '
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,7 +28,7 @@ class FilterService
|
||||||
@filter_values["value_#{current_index}"] = filter_values(query_hash)
|
@filter_values["value_#{current_index}"] = filter_values(query_hash)
|
||||||
equals_to_filter_string(query_hash[:filter_operator], current_index)
|
equals_to_filter_string(query_hash[:filter_operator], current_index)
|
||||||
when 'contains', 'does_not_contain'
|
when 'contains', 'does_not_contain'
|
||||||
@filter_values["value_#{current_index}"] = "%#{filter_values(query_hash)}%"
|
@filter_values["value_#{current_index}"] = "%#{string_filter_values(query_hash)}%"
|
||||||
like_filter_string(query_hash[:filter_operator], current_index)
|
like_filter_string(query_hash[:filter_operator], current_index)
|
||||||
when 'is_present'
|
when 'is_present'
|
||||||
@filter_values["value_#{current_index}"] = 'IS NOT NULL'
|
@filter_values["value_#{current_index}"] = 'IS NOT NULL'
|
||||||
|
@ -57,6 +57,12 @@ class FilterService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def string_filter_values(query_hash)
|
||||||
|
return query_hash['values'][0] if query_hash['values'].is_a?(Array)
|
||||||
|
|
||||||
|
query_hash['values']
|
||||||
|
end
|
||||||
|
|
||||||
def lt_gt_filter_values(query_hash)
|
def lt_gt_filter_values(query_hash)
|
||||||
attribute_key = query_hash[:attribute_key]
|
attribute_key = query_hash[:attribute_key]
|
||||||
attribute_type = custom_attribute(attribute_key).try(:attribute_display_type)
|
attribute_type = custom_attribute(attribute_key).try(:attribute_display_type)
|
||||||
|
@ -81,10 +87,26 @@ class FilterService
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def custom_attribute_query(query_hash, table_name, current_index)
|
||||||
|
attribute_key = query_hash[:attribute_key]
|
||||||
|
query_operator = query_hash[:query_operator]
|
||||||
|
|
||||||
|
attribute_type = custom_attribute(attribute_key, @account).try(:attribute_display_type)
|
||||||
|
filter_operator_value = filter_operation(query_hash, current_index)
|
||||||
|
attribute_data_type = self.class::ATTRIBUTE_TYPES[attribute_type]
|
||||||
|
|
||||||
|
if custom_attribute(attribute_key, @account)
|
||||||
|
" LOWER(#{table_name}.custom_attributes ->> '#{attribute_key}')::#{attribute_data_type} #{filter_operator_value} #{query_operator} "
|
||||||
|
else
|
||||||
|
' '
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def custom_attribute(attribute_key)
|
def custom_attribute(attribute_key, account = nil)
|
||||||
@custom_attribute = Current.account.custom_attribute_definitions.where(
|
current_account = account || Current.account
|
||||||
|
@custom_attribute = current_account.custom_attribute_definitions.where(
|
||||||
attribute_model: self.class::ATTRIBUTE_MODEL
|
attribute_model: self.class::ATTRIBUTE_MODEL
|
||||||
).find_by(attribute_key: attribute_key)
|
).find_by(attribute_key: attribute_key)
|
||||||
end
|
end
|
||||||
|
|
|
@ -62,7 +62,7 @@ class Instagram::SendOnInstagramService < Base::SendOnChannelService
|
||||||
query: query
|
query: query
|
||||||
)
|
)
|
||||||
|
|
||||||
Rails.logger.info("Instagram response: #{response['error']} : #{message_content}") if response['error']
|
Rails.logger.error("Instagram response: #{response['error']} : #{message_content}") if response['error']
|
||||||
message.update!(source_id: response['message_id']) if response['message_id'].present?
|
message.update!(source_id: response['message_id']) if response['message_id'].present?
|
||||||
|
|
||||||
response
|
response
|
||||||
|
|
|
@ -66,7 +66,7 @@ class Notification::PushNotificationService
|
||||||
rescue Webpush::ExpiredSubscription
|
rescue Webpush::ExpiredSubscription
|
||||||
subscription.destroy!
|
subscription.destroy!
|
||||||
rescue Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout => e
|
rescue Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout => e
|
||||||
Rails.logger.info "Webpush operation error: #{e.message}"
|
Rails.logger.error "Webpush operation error: #{e.message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_fcm_push(subscription)
|
def send_fcm_push(subscription)
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
class RoundRobin::AssignmentService
|
class RoundRobin::AssignmentService
|
||||||
pattr_initialize [:conversation]
|
pattr_initialize [:conversation, { allowed_member_ids: [] }]
|
||||||
|
|
||||||
|
def find_assignee
|
||||||
|
round_robin_manage_service.available_agent(priority_list: online_agents)
|
||||||
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
# online agents will get priority
|
# online agents will get priority
|
||||||
new_assignee = round_robin_manage_service.available_agent(priority_list: online_agents)
|
new_assignee = find_assignee
|
||||||
conversation.update(assignee: new_assignee) if new_assignee
|
conversation.update(assignee: new_assignee) if new_assignee
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -15,7 +19,7 @@ class RoundRobin::AssignmentService
|
||||||
end
|
end
|
||||||
|
|
||||||
def round_robin_manage_service
|
def round_robin_manage_service
|
||||||
@round_robin_manage_service ||= RoundRobin::ManageService.new(inbox: conversation.inbox)
|
@round_robin_manage_service ||= RoundRobin::ManageService.new(inbox: conversation.inbox, allowed_member_ids: allowed_member_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
def round_robin_key
|
def round_robin_key
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
# If allowed_member_ids are supplied round robin service will only fetch a member from member id
|
||||||
|
# This is used in case of team assignment
|
||||||
class RoundRobin::ManageService
|
class RoundRobin::ManageService
|
||||||
pattr_initialize [:inbox!]
|
pattr_initialize [:inbox!, { allowed_member_ids: [] }]
|
||||||
|
|
||||||
# called on inbox delete
|
# called on inbox delete
|
||||||
def clear_queue
|
def clear_queue
|
||||||
|
@ -18,9 +20,9 @@ class RoundRobin::ManageService
|
||||||
|
|
||||||
def available_agent(priority_list: [])
|
def available_agent(priority_list: [])
|
||||||
reset_queue unless validate_queue?
|
reset_queue unless validate_queue?
|
||||||
user_id = get_agent_via_priority_list(priority_list)
|
user_id = get_member_via_priority_list(priority_list)
|
||||||
# incase priority list was empty or inbox members weren't present
|
# incase priority list was empty or inbox members weren't present
|
||||||
user_id ||= ::Redis::Alfred.rpoplpush(round_robin_key, round_robin_key)
|
user_id ||= fetch_user_id
|
||||||
inbox.inbox_members.find_by(user_id: user_id)&.user if user_id.present?
|
inbox.inbox_members.find_by(user_id: user_id)&.user if user_id.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -31,17 +33,36 @@ class RoundRobin::ManageService
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def get_agent_via_priority_list(priority_list)
|
def fetch_user_id
|
||||||
|
if allowed_member_ids_in_str.present?
|
||||||
|
user_id = queue.intersection(allowed_member_ids_in_str).pop
|
||||||
|
pop_push_to_queue(user_id)
|
||||||
|
user_id
|
||||||
|
else
|
||||||
|
::Redis::Alfred.rpoplpush(round_robin_key, round_robin_key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# priority list is usually the members who are online passed from assignmebt service
|
||||||
|
def get_member_via_priority_list(priority_list)
|
||||||
|
return if priority_list.blank?
|
||||||
|
|
||||||
|
# when allowed member ids is passed we will be looking to get members from that list alone
|
||||||
|
priority_list = priority_list.intersection(allowed_member_ids_in_str) if allowed_member_ids_in_str.present?
|
||||||
return if priority_list.blank?
|
return if priority_list.blank?
|
||||||
|
|
||||||
user_id = queue.intersection(priority_list.map(&:to_s)).pop
|
user_id = queue.intersection(priority_list.map(&:to_s)).pop
|
||||||
if user_id.present?
|
pop_push_to_queue(user_id)
|
||||||
remove_agent_from_queue(user_id)
|
|
||||||
add_agent_to_queue(user_id)
|
|
||||||
end
|
|
||||||
user_id
|
user_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pop_push_to_queue(user_id)
|
||||||
|
return if user_id.blank?
|
||||||
|
|
||||||
|
remove_agent_from_queue(user_id)
|
||||||
|
add_agent_to_queue(user_id)
|
||||||
|
end
|
||||||
|
|
||||||
def validate_queue?
|
def validate_queue?
|
||||||
return true if inbox.inbox_members.map(&:user_id).sort == queue.map(&:to_i).sort
|
return true if inbox.inbox_members.map(&:user_id).sort == queue.map(&:to_i).sort
|
||||||
end
|
end
|
||||||
|
@ -53,4 +74,9 @@ class RoundRobin::ManageService
|
||||||
def round_robin_key
|
def round_robin_key
|
||||||
format(::Redis::Alfred::ROUND_ROBIN_AGENTS, inbox_id: inbox.id)
|
format(::Redis::Alfred::ROUND_ROBIN_AGENTS, inbox_id: inbox.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def allowed_member_ids_in_str
|
||||||
|
# NOTE: the values which are returned from redis for priority list are string
|
||||||
|
@allowed_member_ids_in_str ||= allowed_member_ids.map(&:to_s)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,14 +5,14 @@ class Twilio::WebhookSetupService
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
if phone_numbers.empty?
|
if phone_numbers.empty?
|
||||||
Rails.logger.info "TWILIO_PHONE_NUMBER_NOT_FOUND: #{channel.phone_number}"
|
Rails.logger.warn "TWILIO_PHONE_NUMBER_NOT_FOUND: #{channel.phone_number}"
|
||||||
else
|
else
|
||||||
twilio_client
|
twilio_client
|
||||||
.incoming_phone_numbers(phonenumber_sid)
|
.incoming_phone_numbers(phonenumber_sid)
|
||||||
.update(sms_method: 'POST', sms_url: twilio_callback_index_url)
|
.update(sms_method: 'POST', sms_url: twilio_callback_index_url)
|
||||||
end
|
end
|
||||||
rescue Twilio::REST::TwilioError => e
|
rescue Twilio::REST::TwilioError => e
|
||||||
Rails.logger.info "TWILIO_FAILURE: #{e.message}"
|
Rails.logger.error "TWILIO_FAILURE: #{e.message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -58,7 +58,7 @@ class Twitter::SendOnTwitterService < Base::SendOnChannelService
|
||||||
tweet_data = response.body
|
tweet_data = response.body
|
||||||
message.update!(source_id: tweet_data['id_str'])
|
message.update!(source_id: tweet_data['id_str'])
|
||||||
else
|
else
|
||||||
Rails.logger.info "TWITTER_TWEET_REPLY_ERROR #{response.body}"
|
Rails.logger.error "TWITTER_TWEET_REPLY_ERROR #{response.body}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,7 +40,7 @@ window.addEventListener('chatwoot:ready', function() {
|
||||||
console.log('chatwoot:ready', window.$chatwoot);
|
console.log('chatwoot:ready', window.$chatwoot);
|
||||||
if (window.location.search.includes('setUser')) {
|
if (window.location.search.includes('setUser')) {
|
||||||
window.$chatwoot.setUser('<%= user_id %>', {
|
window.$chatwoot.setUser('<%= user_id %>', {
|
||||||
identifier_hash: 'a<%= user_hash %>',
|
identifier_hash: '<%= user_hash %>',
|
||||||
email: 'jane@example.com',
|
email: 'jane@example.com',
|
||||||
name: 'Jane Doe',
|
name: 'Jane Doe',
|
||||||
phone_number: ''
|
phone_number: ''
|
||||||
|
@ -49,6 +49,6 @@ window.addEventListener('chatwoot:ready', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
window.addEventListener('chatwoot:error', function(e) {
|
window.addEventListener('chatwoot:error', function(e) {
|
||||||
console.log('chatwoot:error', e.details)
|
console.log('chatwoot:error', e.detail)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -91,7 +91,7 @@ end
|
||||||
|
|
||||||
# Log blocked events
|
# Log blocked events
|
||||||
ActiveSupport::Notifications.subscribe('throttle.rack_attack') do |_name, _start, _finish, _request_id, payload|
|
ActiveSupport::Notifications.subscribe('throttle.rack_attack') do |_name, _start, _finish, _request_id, payload|
|
||||||
Rails.logger.info "[Rack::Attack][Blocked] remote_ip: \"#{payload[:request].remote_ip}\", path: \"#{payload[:request].path}\""
|
Rails.logger.warn "[Rack::Attack][Blocked] remote_ip: \"#{payload[:request].remote_ip}\", path: \"#{payload[:request].path}\""
|
||||||
end
|
end
|
||||||
|
|
||||||
Rack::Attack.enabled = Rails.env.production? ? ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_RACK_ATTACK', true)) : false
|
Rack::Attack.enabled = Rails.env.production? ? ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_RACK_ATTACK', true)) : false
|
||||||
|
|
|
@ -52,8 +52,8 @@ Rails.application.routes.draw do
|
||||||
post :reauthorize_page
|
post :reauthorize_page
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
resources :canned_responses, except: [:show, :edit, :new]
|
resources :canned_responses, only: [:index, :create, :update, :destroy]
|
||||||
resources :automation_rules, except: [:edit] do
|
resources :automation_rules, only: [:index, :create, :show, :update, :destroy] do
|
||||||
post :clone
|
post :clone
|
||||||
end
|
end
|
||||||
resources :campaigns, only: [:index, :create, :show, :update, :destroy]
|
resources :campaigns, only: [:index, :create, :show, :update, :destroy]
|
||||||
|
@ -145,7 +145,7 @@ Rails.application.routes.draw do
|
||||||
resource :authorization, only: [:create]
|
resource :authorization, only: [:create]
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :webhooks, except: [:show]
|
resources :webhooks, only: [:index, :create, :update, :destroy]
|
||||||
namespace :integrations do
|
namespace :integrations do
|
||||||
resources :apps, only: [:index, :show]
|
resources :apps, only: [:index, :show]
|
||||||
resources :hooks, only: [:create, :update, :destroy]
|
resources :hooks, only: [:create, :update, :destroy]
|
||||||
|
@ -212,6 +212,7 @@ Rails.application.routes.draw do
|
||||||
get :inboxes
|
get :inboxes
|
||||||
get :labels
|
get :labels
|
||||||
get :teams
|
get :teams
|
||||||
|
get :conversations
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
- [bots, 1]
|
- [bots, 1]
|
||||||
- [active_storage_analysis, 1]
|
- [active_storage_analysis, 1]
|
||||||
- [action_mailbox_incineration, 1]
|
- [action_mailbox_incineration, 1]
|
||||||
|
- [active_storage_purge, 1]
|
||||||
- [integrations, 2]
|
- [integrations, 2]
|
||||||
- [default, 2]
|
- [default, 2]
|
||||||
- [mailers, 2]
|
- [mailers, 2]
|
||||||
|
|
|
@ -39,7 +39,7 @@ class ChatwootHub
|
||||||
response = RestClient.post(PING_URL, info.to_json, { content_type: :json, accept: :json })
|
response = RestClient.post(PING_URL, info.to_json, { content_type: :json, accept: :json })
|
||||||
version = JSON.parse(response)['version']
|
version = JSON.parse(response)['version']
|
||||||
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
|
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
|
||||||
Rails.logger.info "Exception: #{e.message}"
|
Rails.logger.error "Exception: #{e.message}"
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Sentry.capture_exception(e)
|
Sentry.capture_exception(e)
|
||||||
end
|
end
|
||||||
|
@ -50,7 +50,7 @@ class ChatwootHub
|
||||||
info = { company_name: company_name, owner_name: owner_name, owner_email: owner_email, subscribed_to_mailers: true }
|
info = { company_name: company_name, owner_name: owner_name, owner_email: owner_email, subscribed_to_mailers: true }
|
||||||
RestClient.post(REGISTRATION_URL, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
|
RestClient.post(REGISTRATION_URL, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
|
||||||
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
|
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
|
||||||
Rails.logger.info "Exception: #{e.message}"
|
Rails.logger.error "Exception: #{e.message}"
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Sentry.capture_exception(e)
|
Sentry.capture_exception(e)
|
||||||
end
|
end
|
||||||
|
@ -59,7 +59,7 @@ class ChatwootHub
|
||||||
info = { fcm_token_list: fcm_token_list, fcm_options: fcm_options }
|
info = { fcm_token_list: fcm_token_list, fcm_options: fcm_options }
|
||||||
RestClient.post(PUSH_NOTIFICATION_URL, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
|
RestClient.post(PUSH_NOTIFICATION_URL, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
|
||||||
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
|
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
|
||||||
Rails.logger.info "Exception: #{e.message}"
|
Rails.logger.error "Exception: #{e.message}"
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Sentry.capture_exception(e)
|
Sentry.capture_exception(e)
|
||||||
end
|
end
|
||||||
|
@ -70,7 +70,7 @@ class ChatwootHub
|
||||||
info = { event_name: event_name, event_data: event_data }
|
info = { event_name: event_name, event_data: event_data }
|
||||||
RestClient.post(EVENTS_URL, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
|
RestClient.post(EVENTS_URL, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
|
||||||
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
|
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
|
||||||
Rails.logger.info "Exception: #{e.message}"
|
Rails.logger.error "Exception: #{e.message}"
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Sentry.capture_exception(e)
|
Sentry.capture_exception(e)
|
||||||
end
|
end
|
||||||
|
|
|
@ -142,6 +142,13 @@
|
||||||
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ],
|
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ],
|
||||||
"attribute_type": "additional_attributes"
|
"attribute_type": "additional_attributes"
|
||||||
},
|
},
|
||||||
|
"company": {
|
||||||
|
"attribute_name": "Company",
|
||||||
|
"input_type": "textbox",
|
||||||
|
"data_type": "text",
|
||||||
|
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ],
|
||||||
|
"attribute_type": "additional_attributes"
|
||||||
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"attribute_name": "Labels",
|
"attribute_name": "Labels",
|
||||||
"input_type": "tags",
|
"input_type": "tags",
|
||||||
|
|
|
@ -48,7 +48,7 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
|
||||||
post_message if message_content.present?
|
post_message if message_content.present?
|
||||||
upload_file if message.attachments.any?
|
upload_file if message.attachments.any?
|
||||||
rescue Slack::Web::Api::Errors::AccountInactive => e
|
rescue Slack::Web::Api::Errors::AccountInactive => e
|
||||||
Rails.logger.info e
|
Rails.logger.error e
|
||||||
hook.authorization_error!
|
hook.authorization_error!
|
||||||
hook.disable if hook.enabled?
|
hook.disable if hook.enabled?
|
||||||
end
|
end
|
||||||
|
|
|
@ -84,6 +84,23 @@ RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'throws an error for unknown attributes in condtions' do
|
||||||
|
expect(account.automation_rules.count).to eq(0)
|
||||||
|
params[:conditions] << {
|
||||||
|
attribute_key: 'unknown_attribute',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['en'],
|
||||||
|
query_operator: 'AND'
|
||||||
|
}
|
||||||
|
|
||||||
|
post "/api/v1/accounts/#{account.id}/automation_rules",
|
||||||
|
headers: administrator.create_new_auth_token,
|
||||||
|
params: params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(account.automation_rules.count).to eq(0)
|
||||||
|
end
|
||||||
|
|
||||||
it 'Saves for automation_rules for account with country_code and browser_language conditions' do
|
it 'Saves for automation_rules for account with country_code and browser_language conditions' do
|
||||||
expect(account.automation_rules.count).to eq(0)
|
expect(account.automation_rules.count).to eq(0)
|
||||||
|
|
||||||
|
@ -113,6 +130,67 @@ RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
expect(account.automation_rules.count).to eq(1)
|
expect(account.automation_rules.count).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'Saves file in the automation actions to send an attachments' do
|
||||||
|
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
||||||
|
params[:attachments] = [file]
|
||||||
|
params[:actions] = [
|
||||||
|
{
|
||||||
|
action_name: :send_message,
|
||||||
|
action_params: ['Welcome to the chatwoot platform.']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action_name: :update_additional_attributes,
|
||||||
|
action_params: [{ intiated_at: '2021-12-03 17:25:26.844536 +0530' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action_name: :send_attachments
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(account.automation_rules.count).to eq(0)
|
||||||
|
|
||||||
|
post "/api/v1/accounts/#{account.id}/automation_rules",
|
||||||
|
headers: administrator.create_new_auth_token,
|
||||||
|
params: params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(account.automation_rules.count).to eq(1)
|
||||||
|
automation_rule = account.automation_rules.first
|
||||||
|
expect(automation_rule.files.presence).to be_truthy
|
||||||
|
expect(automation_rule.files.count).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'Saves files in the automation actions to send multiple attachments' do
|
||||||
|
file_1 = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
||||||
|
file_2 = fixture_file_upload(Rails.root.join('spec/assets/sample.pdf'), 'image/png')
|
||||||
|
params[:attachments] = [file_1, file_2]
|
||||||
|
params[:actions] = [
|
||||||
|
{
|
||||||
|
action_name: :send_message,
|
||||||
|
action_params: ['Welcome to the chatwoot platform.']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action_name: :update_additional_attributes,
|
||||||
|
action_params: [{ intiated_at: '2021-12-03 17:25:26.844536 +0530' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action_name: :send_attachments
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(account.automation_rules.count).to eq(0)
|
||||||
|
|
||||||
|
post "/api/v1/accounts/#{account.id}/automation_rules",
|
||||||
|
headers: administrator.create_new_auth_token,
|
||||||
|
params: params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(account.automation_rules.count).to eq(1)
|
||||||
|
automation_rule = account.automation_rules.first
|
||||||
|
expect(automation_rule.files.presence).to be_truthy
|
||||||
|
expect(automation_rule.files.count).to eq(2)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,9 @@ RSpec.describe 'Conversation Assignment API', type: :request do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'assigns a team to the conversation' do
|
it 'assigns a team to the conversation' do
|
||||||
|
team_member = create(:user, account: account, role: :agent)
|
||||||
|
create(:inbox_member, inbox: conversation.inbox, user: team_member)
|
||||||
|
create(:team_member, team: team, user: team_member)
|
||||||
params = { team_id: team.id }
|
params = { team_id: team.id }
|
||||||
|
|
||||||
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id),
|
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id),
|
||||||
|
@ -44,6 +47,8 @@ RSpec.describe 'Conversation Assignment API', type: :request do
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
expect(conversation.reload.team).to eq(team)
|
expect(conversation.reload.team).to eq(team)
|
||||||
|
# assignee will be from team
|
||||||
|
expect(conversation.reload.assignee).to eq(team_member)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,8 @@ RSpec.describe 'Reports API', type: :request do
|
||||||
let!(:user) { create(:user, account: account) }
|
let!(:user) { create(:user, account: account) }
|
||||||
let!(:inbox) { create(:inbox, account: account) }
|
let!(:inbox) { create(:inbox, account: account) }
|
||||||
let(:inbox_member) { create(:inbox_member, user: user, inbox: inbox) }
|
let(:inbox_member) { create(:inbox_member, user: user, inbox: inbox) }
|
||||||
let(:date_timestamp) { Time.current.beginning_of_day.to_i }
|
let(:default_timezone) { ActiveSupport::TimeZone[0]&.name }
|
||||||
|
let(:date_timestamp) { Time.current.in_time_zone(default_timezone).beginning_of_day.to_i }
|
||||||
let(:params) { { timezone_offset: Time.zone.utc_offset } }
|
let(:params) { { timezone_offset: Time.zone.utc_offset } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
@ -56,6 +57,68 @@ RSpec.describe 'Reports API', type: :request do
|
||||||
expect(current_day_metric.length).to eq(1)
|
expect(current_day_metric.length).to eq(1)
|
||||||
expect(current_day_metric[0]['value']).to eq(10)
|
expect(current_day_metric[0]['value']).to eq(10)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'return conversation metrics in account level' do
|
||||||
|
unassigned_conversation = create(:conversation, account: account, inbox: inbox,
|
||||||
|
assignee: nil, created_at: Time.zone.today)
|
||||||
|
unassigned_conversation.save!
|
||||||
|
|
||||||
|
get "/api/v2/accounts/#{account.id}/reports/conversations",
|
||||||
|
params: {
|
||||||
|
type: :account
|
||||||
|
},
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
|
||||||
|
expect(json_response['open']).to eq(11)
|
||||||
|
expect(json_response['unattended']).to eq(11)
|
||||||
|
expect(json_response['unassigned']).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'return conversation metrics for user in account level' do
|
||||||
|
create_list(:conversation, 2, account: account, inbox: inbox,
|
||||||
|
assignee: admin, created_at: Time.zone.today)
|
||||||
|
|
||||||
|
get "/api/v2/accounts/#{account.id}/reports/conversations",
|
||||||
|
params: {
|
||||||
|
type: :agent
|
||||||
|
},
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response.blank?).to be false
|
||||||
|
user_metrics = json_response.find { |item| item['user']['name'] == admin[:name] }
|
||||||
|
expect(user_metrics.present?).to be true
|
||||||
|
|
||||||
|
expect(user_metrics['metric']['open']).to eq(2)
|
||||||
|
expect(user_metrics['metric']['unattended']).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'return conversation metrics for specific user in account level' do
|
||||||
|
create_list(:conversation, 2, account: account, inbox: inbox,
|
||||||
|
assignee: admin, created_at: Time.zone.today)
|
||||||
|
|
||||||
|
get "/api/v2/accounts/#{account.id}/reports/conversations",
|
||||||
|
params: {
|
||||||
|
type: :agent,
|
||||||
|
user_id: user.id
|
||||||
|
},
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response.blank?).to be false
|
||||||
|
expect(json_response[0]['metric']['open']).to eq(10)
|
||||||
|
expect(json_response[0]['metric']['unattended']).to eq(10)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ if defined?(ActiveRecord::Base)
|
||||||
ActiveRecord::Base.descendants.each_with_object({}) do |record_class, records|
|
ActiveRecord::Base.descendants.each_with_object({}) do |record_class, records|
|
||||||
records[record_class.to_s] = record_class.limit(100).map(&:attributes)
|
records[record_class.to_s] = record_class.limit(100).map(&:attributes)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.info e.message
|
Rails.logger.error e.message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,12 @@ describe AutomationRuleListener do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
create(:custom_attribute_definition,
|
||||||
|
attribute_key: 'customer_type',
|
||||||
|
account: account,
|
||||||
|
attribute_model: 'contact_attribute',
|
||||||
|
attribute_display_type: 'list',
|
||||||
|
attribute_values: %w[regular platinum gold])
|
||||||
create(:team_member, user: user_1, team: team)
|
create(:team_member, user: user_1, team: team)
|
||||||
create(:team_member, user: user_2, team: team)
|
create(:team_member, user: user_2, team: team)
|
||||||
create(:account_user, user: user_2, account: account)
|
create(:account_user, user: user_2, account: account)
|
||||||
|
@ -31,13 +37,17 @@ describe AutomationRuleListener do
|
||||||
},
|
},
|
||||||
{ 'action_name' => 'assign_team', 'action_params' => [team.id] },
|
{ 'action_name' => 'assign_team', 'action_params' => [team.id] },
|
||||||
{ 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] },
|
{ 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] },
|
||||||
{ 'action_name' => 'send_webhook_events', 'action_params' => 'https://www.example.com' },
|
{ 'action_name' => 'send_webhook_event', 'action_params' => ['https://www.example.com'] },
|
||||||
{ 'action_name' => 'assign_best_agent', 'action_params' => [user_1.id] },
|
{ 'action_name' => 'assign_best_agent', 'action_params' => [user_1.id] },
|
||||||
{ 'action_name' => 'send_email_transcript', 'action_params' => 'new_agent@example.com' },
|
{ 'action_name' => 'send_email_transcript', 'action_params' => 'new_agent@example.com' },
|
||||||
{ 'action_name' => 'mute_conversation', 'action_params' => nil },
|
{ 'action_name' => 'mute_conversation', 'action_params' => nil },
|
||||||
{ 'action_name' => 'change_status', 'action_params' => ['snoozed'] },
|
{ 'action_name' => 'change_status', 'action_params' => ['snoozed'] },
|
||||||
{ 'action_name' => 'send_message', 'action_params' => ['Send this message.'] }
|
{ 'action_name' => 'send_message', 'action_params' => ['Send this message.'] },
|
||||||
|
{ 'action_name' => 'send_attachments' }
|
||||||
])
|
])
|
||||||
|
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
||||||
|
automation_rule.files.attach(file)
|
||||||
|
automation_rule.save
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#conversation_status_changed' do
|
describe '#conversation_status_changed' do
|
||||||
|
@ -77,6 +87,7 @@ describe AutomationRuleListener do
|
||||||
listener.conversation_status_changed(event)
|
listener.conversation_status_changed(event)
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer')
|
expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -105,7 +116,7 @@ describe AutomationRuleListener do
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
expect(conversation.messages.last.content).to eq('Send this message.')
|
expect(conversation.messages.first.content).to eq('Send this message.')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'triggers automation rule changes status to snoozed' do
|
it 'triggers automation rule changes status to snoozed' do
|
||||||
|
@ -143,6 +154,120 @@ describe AutomationRuleListener do
|
||||||
|
|
||||||
listener.conversation_status_changed(event)
|
listener.conversation_status_changed(event)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'triggers automation rule send attachments in messages' do
|
||||||
|
automation_rule
|
||||||
|
|
||||||
|
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
|
||||||
|
|
||||||
|
listener.conversation_status_changed(event)
|
||||||
|
|
||||||
|
conversation.reload
|
||||||
|
|
||||||
|
expect(conversation.messages.last.attachments.count).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#conversation_updated with contacts attributes' do
|
||||||
|
before do
|
||||||
|
conversation.contact.update!(custom_attributes: { customer_type: 'platinum', signed_in_at: '2022-01-19' },
|
||||||
|
additional_attributes: { 'company': 'Marvel' })
|
||||||
|
|
||||||
|
automation_rule.update!(
|
||||||
|
event_name: 'conversation_updated',
|
||||||
|
name: 'Call actions conversation updated',
|
||||||
|
description: 'Add labels, assign team after conversation updated',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
attribute_key: 'company',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['Marvel'],
|
||||||
|
query_operator: 'AND'
|
||||||
|
}.with_indifferent_access,
|
||||||
|
{
|
||||||
|
attribute_key: 'customer_type',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['platinum'],
|
||||||
|
query_operator: nil
|
||||||
|
}.with_indifferent_access
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:event) do
|
||||||
|
Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation })
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when rule matches with additional_attributes and custom_attributes' do
|
||||||
|
it 'triggers automation rule to assign team' do
|
||||||
|
expect(conversation.team_id).not_to eq(team.id)
|
||||||
|
|
||||||
|
listener.conversation_updated(event)
|
||||||
|
|
||||||
|
conversation.reload
|
||||||
|
expect(conversation.team_id).to eq(team.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'triggers automation rule to add label and assign best agents' do
|
||||||
|
expect(conversation.labels).to eq([])
|
||||||
|
expect(conversation.assignee).to be_nil
|
||||||
|
|
||||||
|
listener.conversation_updated(event)
|
||||||
|
|
||||||
|
conversation.reload
|
||||||
|
expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer')
|
||||||
|
expect(conversation.assignee).to eq(user_1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'triggers automation rule send email transcript to the mentioned email' do
|
||||||
|
mailer = double
|
||||||
|
|
||||||
|
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_updated)
|
||||||
|
|
||||||
|
listener.conversation_updated(event)
|
||||||
|
|
||||||
|
conversation.reload
|
||||||
|
|
||||||
|
allow(mailer).to receive(:conversation_transcript)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'triggers automation rule send message to the contacts' do
|
||||||
|
expect(conversation.messages).to be_empty
|
||||||
|
|
||||||
|
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_updated)
|
||||||
|
|
||||||
|
listener.conversation_updated(event)
|
||||||
|
|
||||||
|
conversation.reload
|
||||||
|
|
||||||
|
expect(conversation.messages.first.content).to eq('Send this message.')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'triggers automation_rule with contact standard attributes' do
|
||||||
|
automation_rule.update!(
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
attribute_key: 'email',
|
||||||
|
filter_operator: 'contains',
|
||||||
|
values: ['example.com'],
|
||||||
|
query_operator: 'AND'
|
||||||
|
}.with_indifferent_access,
|
||||||
|
{
|
||||||
|
attribute_key: 'customer_type',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['platinum'],
|
||||||
|
query_operator: nil
|
||||||
|
}.with_indifferent_access
|
||||||
|
]
|
||||||
|
)
|
||||||
|
expect(conversation.team_id).not_to eq(team.id)
|
||||||
|
|
||||||
|
listener.conversation_updated(event)
|
||||||
|
|
||||||
|
conversation.reload
|
||||||
|
expect(conversation.team_id).to eq(team.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -224,7 +349,7 @@ describe AutomationRuleListener do
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
expect(conversation.messages.last.content).to eq('Send this message.')
|
expect(conversation.messages.first.content).to eq('Send this message.')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -105,4 +105,80 @@ describe WebhookListener do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#conversation_resolved' do
|
||||||
|
let!(:conversation_resolved_event) do
|
||||||
|
Events::Base.new(event_name, Time.zone.now, conversation: conversation.reload, changed_attributes: { status: [:open, :resolved] })
|
||||||
|
end
|
||||||
|
let(:event_name) { :'conversation.resolved' }
|
||||||
|
|
||||||
|
context 'when webhook is not configured' do
|
||||||
|
it 'does not trigger webhook' do
|
||||||
|
expect(WebhookJob).to receive(:perform_later).exactly(0).times
|
||||||
|
listener.conversation_resolved(conversation_resolved_event)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when webhook is configured' do
|
||||||
|
it 'triggers webhook' do
|
||||||
|
webhook = create(:webhook, inbox: inbox, account: account)
|
||||||
|
|
||||||
|
conversation.update(status: :resolved)
|
||||||
|
|
||||||
|
expect(WebhookJob).to receive(:perform_later).with(webhook.url,
|
||||||
|
conversation.webhook_data.merge(event: 'conversation_resolved',
|
||||||
|
changed_attributes: [{ status: {
|
||||||
|
current_value: :resolved, previous_value: :open
|
||||||
|
} }])).once
|
||||||
|
|
||||||
|
listener.conversation_resolved(conversation_resolved_event)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#conversation_updated' do
|
||||||
|
let(:custom_attributes) { { test: nil } }
|
||||||
|
let!(:conversation_updated_event) do
|
||||||
|
Events::Base.new(
|
||||||
|
event_name, Time.zone.now,
|
||||||
|
conversation: conversation.reload,
|
||||||
|
changed_attributes: {
|
||||||
|
custom_attributes: [{ test: nil }, { test: 'testing custom attri webhook' }]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
let(:event_name) { :'conversation.updated' }
|
||||||
|
|
||||||
|
context 'when webhook is not configured' do
|
||||||
|
it 'does not trigger webhook' do
|
||||||
|
expect(WebhookJob).to receive(:perform_later).exactly(0).times
|
||||||
|
listener.conversation_updated(conversation_updated_event)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when webhook is configured' do
|
||||||
|
it 'triggers webhook' do
|
||||||
|
webhook = create(:webhook, inbox: inbox, account: account)
|
||||||
|
|
||||||
|
conversation.update(custom_attributes: { test: 'testing custom attri webhook' })
|
||||||
|
|
||||||
|
expect(WebhookJob).to receive(:perform_later).with(
|
||||||
|
webhook.url,
|
||||||
|
conversation.webhook_data.merge(
|
||||||
|
event: 'conversation_updated',
|
||||||
|
changed_attributes: [
|
||||||
|
{
|
||||||
|
custom_attributes: {
|
||||||
|
previous_value: { test: nil },
|
||||||
|
current_value: { test: 'testing custom attri webhook' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
).once
|
||||||
|
|
||||||
|
listener.conversation_updated(conversation_updated_event)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -54,7 +54,8 @@ RSpec.describe Conversation, type: :model do
|
||||||
it 'runs after_create callbacks' do
|
it 'runs after_create callbacks' do
|
||||||
# send_events
|
# send_events
|
||||||
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
||||||
.with(described_class::CONVERSATION_CREATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: false)
|
.with(described_class::CONVERSATION_CREATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: false,
|
||||||
|
changed_attributes: nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -115,14 +116,21 @@ RSpec.describe Conversation, type: :model do
|
||||||
assignee: new_assignee,
|
assignee: new_assignee,
|
||||||
label_list: [label.title]
|
label_list: [label.title]
|
||||||
)
|
)
|
||||||
|
status_change = conversation.status_change
|
||||||
|
changed_attributes = conversation.previous_changes
|
||||||
|
|
||||||
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
||||||
.with(described_class::CONVERSATION_RESOLVED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
.with(described_class::CONVERSATION_RESOLVED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true,
|
||||||
|
changed_attributes: status_change)
|
||||||
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
||||||
.with(described_class::CONVERSATION_READ, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
.with(described_class::CONVERSATION_READ, kind_of(Time), conversation: conversation, notifiable_assignee_change: true,
|
||||||
|
changed_attributes: nil)
|
||||||
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
||||||
.with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
.with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true,
|
||||||
|
changed_attributes: nil)
|
||||||
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
||||||
.with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
.with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true,
|
||||||
|
changed_attributes: changed_attributes)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'will not run conversation_updated event for empty updates' do
|
it 'will not run conversation_updated event for empty updates' do
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe RoundRobin::ManageService do
|
describe RoundRobin::ManageService do
|
||||||
subject(:round_robin_manage_service) { ::RoundRobin::ManageService.new(inbox: inbox) }
|
subject(:round_robin_manage_service) { described_class.new(inbox: inbox) }
|
||||||
|
|
||||||
let!(:account) { create(:account) }
|
let!(:account) { create(:account) }
|
||||||
let!(:inbox) { create(:inbox, account: account) }
|
let!(:inbox) { create(:inbox, account: account) }
|
||||||
|
@ -18,8 +18,9 @@ describe RoundRobin::ManageService do
|
||||||
it 'gets intersection of priority list and agent queue. get and move agent to the end of the list' do
|
it 'gets intersection of priority list and agent queue. get and move agent to the end of the list' do
|
||||||
expected_queue = [inbox_members[2].user_id, inbox_members[4].user_id, inbox_members[3].user_id, inbox_members[1].user_id,
|
expected_queue = [inbox_members[2].user_id, inbox_members[4].user_id, inbox_members[3].user_id, inbox_members[1].user_id,
|
||||||
inbox_members[0].user_id].map(&:to_s)
|
inbox_members[0].user_id].map(&:to_s)
|
||||||
expect(round_robin_manage_service.available_agent(priority_list: [inbox_members[3].user_id,
|
# prority list will be ids in string, since thats what redis supplies to us
|
||||||
inbox_members[2].user_id])).to eq inbox_members[2].user
|
expect(round_robin_manage_service.available_agent(priority_list: [inbox_members[3].user_id.to_s,
|
||||||
|
inbox_members[2].user_id.to_s])).to eq inbox_members[2].user
|
||||||
expect(round_robin_manage_service.send(:queue)).to eq(expected_queue)
|
expect(round_robin_manage_service.send(:queue)).to eq(expected_queue)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -39,5 +40,32 @@ describe RoundRobin::ManageService do
|
||||||
# the service have refreshed the redis queue before performing
|
# the service have refreshed the redis queue before performing
|
||||||
expect(round_robin_manage_service.send(:queue).sort.map(&:to_i)).to eq(inbox_members.map(&:user_id).sort)
|
expect(round_robin_manage_service.send(:queue).sort.map(&:to_i)).to eq(inbox_members.map(&:user_id).sort)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when allowed_member_ids is passed' do
|
||||||
|
it 'will get the first allowed member and move it to the end of the queue' do
|
||||||
|
expected_queue = [inbox_members[3].user_id, inbox_members[2].user_id, inbox_members[4].user_id, inbox_members[1].user_id,
|
||||||
|
inbox_members[0].user_id].map(&:to_s)
|
||||||
|
expect(described_class.new(inbox: inbox,
|
||||||
|
allowed_member_ids: [inbox_members[3].user_id,
|
||||||
|
inbox_members[2].user_id]).available_agent).to eq inbox_members[2].user
|
||||||
|
expect(described_class.new(inbox: inbox,
|
||||||
|
allowed_member_ids: [inbox_members[3].user_id,
|
||||||
|
inbox_members[2].user_id]).available_agent).to eq inbox_members[3].user
|
||||||
|
expect(round_robin_manage_service.send(:queue)).to eq(expected_queue)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'will get union of priority list and allowed_member_ids and move it to the end of the queue' do
|
||||||
|
expected_queue = [inbox_members[3].user_id, inbox_members[4].user_id, inbox_members[2].user_id, inbox_members[1].user_id,
|
||||||
|
inbox_members[0].user_id].map(&:to_s)
|
||||||
|
# prority list will be ids in string, since thats what redis supplies to us
|
||||||
|
expect(described_class.new(inbox: inbox,
|
||||||
|
allowed_member_ids: [inbox_members[3].user_id,
|
||||||
|
inbox_members[2].user_id])
|
||||||
|
.available_agent(
|
||||||
|
priority_list: [inbox_members[3].user_id.to_s]
|
||||||
|
)).to eq inbox_members[3].user
|
||||||
|
expect(round_robin_manage_service.send(:queue)).to eq(expected_queue)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,6 +12,8 @@ request_error:
|
||||||
|
|
||||||
generic_id:
|
generic_id:
|
||||||
$ref: ./resource/extension/generic.yml
|
$ref: ./resource/extension/generic.yml
|
||||||
|
canned_response:
|
||||||
|
$ref: ./resource/canned_response.yml
|
||||||
contact:
|
contact:
|
||||||
$ref: ./resource/contact.yml
|
$ref: ./resource/contact.yml
|
||||||
conversation:
|
conversation:
|
||||||
|
@ -64,6 +66,9 @@ agent_bot_create_update_payload:
|
||||||
user_create_update_payload:
|
user_create_update_payload:
|
||||||
$ref: ./request/user/create_update_payload.yml
|
$ref: ./request/user/create_update_payload.yml
|
||||||
|
|
||||||
|
canned_response_create_update_payload:
|
||||||
|
$ref: ./request/canned_response/create_update_payload.yml
|
||||||
|
|
||||||
## contact
|
## contact
|
||||||
contact_create:
|
contact_create:
|
||||||
$ref: ./request/contact/create.yml
|
$ref: ./request/contact/create.yml
|
||||||
|
@ -144,10 +149,11 @@ extended_message:
|
||||||
- $ref: ./resource/extension/message/with_source_sender.yml
|
- $ref: ./resource/extension/message/with_source_sender.yml
|
||||||
|
|
||||||
|
|
||||||
## report list
|
## report
|
||||||
report:
|
account_summary:
|
||||||
type: array
|
$ref: './resource/reports/summary.yml'
|
||||||
description: 'array of conversation count based on date'
|
agent_conversation_metrics:
|
||||||
items:
|
$ref: './resource/reports/conversation/agent.yml'
|
||||||
allOf:
|
|
||||||
- $ref: './resource/report.yml'
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
content:
|
||||||
|
type: string
|
||||||
|
description: Message content for canned response
|
||||||
|
short_code:
|
||||||
|
type: string
|
||||||
|
description: Short Code for quick access of the canned response
|
14
swagger/definitions/resource/canned_response.yml
Normal file
14
swagger/definitions/resource/canned_response.yml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
description: ID of the canned response
|
||||||
|
content:
|
||||||
|
type: string
|
||||||
|
description: Message content for canned response
|
||||||
|
short_code:
|
||||||
|
type: string
|
||||||
|
description: Short Code for quick access of the canned response
|
||||||
|
account_id:
|
||||||
|
type: integer
|
||||||
|
description: Account Id
|
18
swagger/definitions/resource/reports/conversation/agent.yml
Normal file
18
swagger/definitions/resource/reports/conversation/agent.yml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
user:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: number
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
thumbnail:
|
||||||
|
type: string
|
||||||
|
metric:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
open:
|
||||||
|
type: number
|
||||||
|
unattended:
|
||||||
|
type: number
|
|
@ -54,6 +54,7 @@ x-tagGroups:
|
||||||
tags:
|
tags:
|
||||||
- Account AgentBots
|
- Account AgentBots
|
||||||
- Agent
|
- Agent
|
||||||
|
- Canned Response
|
||||||
- Contact
|
- Contact
|
||||||
- Conversation
|
- Conversation
|
||||||
- Conversation Assignment
|
- Conversation Assignment
|
||||||
|
|
21
swagger/paths/application/canned_responses/create.yml
Normal file
21
swagger/paths/application/canned_responses/create.yml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
tags:
|
||||||
|
- Canned Response
|
||||||
|
operationId: add-new-canned-response-to-account
|
||||||
|
summary: Add a New Canned Response
|
||||||
|
description: Add a new Canned Response to Account
|
||||||
|
security:
|
||||||
|
- userApiKey: []
|
||||||
|
parameters:
|
||||||
|
- name: data
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/canned_response_create_update_payload'
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Success
|
||||||
|
schema:
|
||||||
|
description: 'Newly Created Canned Response'
|
||||||
|
$ref: '#/definitions/canned_response'
|
||||||
|
403:
|
||||||
|
description: Access denied
|
20
swagger/paths/application/canned_responses/delete.yml
Normal file
20
swagger/paths/application/canned_responses/delete.yml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
tags:
|
||||||
|
- Canned Response
|
||||||
|
operationId: delete-canned-response-from-account
|
||||||
|
summary: Remove a Canned Response from Account
|
||||||
|
description: Remove a Canned Response from Account
|
||||||
|
security:
|
||||||
|
- userApiKey: []
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
type: integer
|
||||||
|
required: true
|
||||||
|
description: The ID of the canned response to be deleted
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Success
|
||||||
|
404:
|
||||||
|
description: Canned Response not found
|
||||||
|
403:
|
||||||
|
description: Access denied
|
17
swagger/paths/application/canned_responses/index.yml
Normal file
17
swagger/paths/application/canned_responses/index.yml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
tags:
|
||||||
|
- Canned Response
|
||||||
|
operationId: get-account-canned-response
|
||||||
|
summary: List all Canned Responses in an Account
|
||||||
|
description: Get Details of Canned Responses in an Account
|
||||||
|
security:
|
||||||
|
- userApiKey: []
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Success
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
description: 'Array of all canned responses'
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/canned_response'
|
||||||
|
403:
|
||||||
|
description: Access denied
|
28
swagger/paths/application/canned_responses/update.yml
Normal file
28
swagger/paths/application/canned_responses/update.yml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
tags:
|
||||||
|
- Canned Response
|
||||||
|
operationId: update-canned-response-in-account
|
||||||
|
summary: Update Canned Response in Account
|
||||||
|
description: Update a Canned Response in Account
|
||||||
|
security:
|
||||||
|
- userApiKey: []
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
type: integer
|
||||||
|
required: true
|
||||||
|
description: The ID of the canned response to be updated.
|
||||||
|
- name: data
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/canned_response_create_update_payload'
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Success
|
||||||
|
schema:
|
||||||
|
description: 'The updated canned response'
|
||||||
|
$ref: '#/definitions/canned_response'
|
||||||
|
404:
|
||||||
|
description: Agent not found
|
||||||
|
403:
|
||||||
|
description: Access denied
|
23
swagger/paths/application/reports/conversation/account.yml
Normal file
23
swagger/paths/application/reports/conversation/account.yml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
tags:
|
||||||
|
- Reports
|
||||||
|
operationId: get-account-conversation-metrics
|
||||||
|
summary: Account Conversation Metrics
|
||||||
|
description: Get conversation metrics for Account
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Success
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
description: 'Object of account conversation metrics'
|
||||||
|
properties:
|
||||||
|
open:
|
||||||
|
type: number
|
||||||
|
unattended:
|
||||||
|
type: number
|
||||||
|
unassigned:
|
||||||
|
type: number
|
||||||
|
|
||||||
|
404:
|
||||||
|
description: reports not found
|
||||||
|
403:
|
||||||
|
description: Access denied
|
18
swagger/paths/application/reports/conversation/agent.yml
Normal file
18
swagger/paths/application/reports/conversation/agent.yml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
tags:
|
||||||
|
- Reports
|
||||||
|
operationId: get-agent-conversation-metrics
|
||||||
|
summary: Agent Conversation Metrics
|
||||||
|
description: Get conversation metrics for Agent
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Success
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
description: 'Array of agent based conversation metrics'
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/agent_conversation_metrics'
|
||||||
|
|
||||||
|
404:
|
||||||
|
description: reports not found
|
||||||
|
403:
|
||||||
|
description: Access denied
|
|
@ -10,7 +10,12 @@ responses:
|
||||||
type: array
|
type: array
|
||||||
description: 'Array of date based conversation statistics'
|
description: 'Array of date based conversation statistics'
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/report'
|
type: object
|
||||||
|
properties:
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
timestamp:
|
||||||
|
type: number
|
||||||
404:
|
404:
|
||||||
description: reports not found
|
description: reports not found
|
||||||
403:
|
403:
|
||||||
|
|
|
@ -7,10 +7,8 @@ responses:
|
||||||
200:
|
200:
|
||||||
description: Success
|
description: Success
|
||||||
schema:
|
schema:
|
||||||
type: array
|
description: 'Object of summary metrics'
|
||||||
description: 'Array of date based conversation statistics'
|
$ref: '#/definitions/account_summary'
|
||||||
items:
|
|
||||||
$ref: '#/definitions/report'
|
|
||||||
404:
|
404:
|
||||||
description: reports not found
|
description: reports not found
|
||||||
403:
|
403:
|
||||||
|
|
|
@ -154,6 +154,21 @@
|
||||||
delete:
|
delete:
|
||||||
$ref: ./application/agents/delete.yml
|
$ref: ./application/agents/delete.yml
|
||||||
|
|
||||||
|
# Agents
|
||||||
|
/api/v1/accounts/{account_id}/canned_responses:
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/parameters/account_id'
|
||||||
|
get:
|
||||||
|
$ref: ./application/canned_responses/index.yml
|
||||||
|
post:
|
||||||
|
$ref: ./application/canned_responses/create.yml
|
||||||
|
/api/v1/accounts/{account_id}/canned_responses/{id}:
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/parameters/account_id'
|
||||||
|
patch:
|
||||||
|
$ref: ./application/canned_responses/update.yml
|
||||||
|
delete:
|
||||||
|
$ref: ./application/canned_responses/delete.yml
|
||||||
|
|
||||||
# Contacts
|
# Contacts
|
||||||
/api/v1/accounts/{account_id}/contacts:
|
/api/v1/accounts/{account_id}/contacts:
|
||||||
|
@ -376,3 +391,35 @@
|
||||||
description: The timestamp from where report should stop.
|
description: The timestamp from where report should stop.
|
||||||
get:
|
get:
|
||||||
$ref: './application/reports/summary.yml'
|
$ref: './application/reports/summary.yml'
|
||||||
|
|
||||||
|
# Conversation metrics for account
|
||||||
|
/api/v2/accounts/{account_id}/reports/conversations:
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/parameters/account_id'
|
||||||
|
- in: query
|
||||||
|
name: type
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- account
|
||||||
|
required: true
|
||||||
|
description: Type of report
|
||||||
|
get:
|
||||||
|
$ref: './application/reports/conversation/account.yml'
|
||||||
|
|
||||||
|
# Conversation metrics for agent
|
||||||
|
/api/v2/accounts/{account_id}/reports/conversations/:
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/parameters/account_id'
|
||||||
|
- in: query
|
||||||
|
name: type
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- agent
|
||||||
|
required: true
|
||||||
|
description: Type of report
|
||||||
|
- in: query
|
||||||
|
name: user_id
|
||||||
|
type: string
|
||||||
|
description: The numeric ID of the user
|
||||||
|
get:
|
||||||
|
$ref: './application/reports/conversation/agent.yml'
|
|
@ -1333,6 +1333,167 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/accounts/{account_id}/canned_responses": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "#/parameters/account_id"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Canned Response"
|
||||||
|
],
|
||||||
|
"operationId": "get-account-canned-response",
|
||||||
|
"summary": "List all Canned Responses in an Account",
|
||||||
|
"description": "Get Details of Canned Responses in an Account",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"userApiKey": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of all canned responses",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/canned_response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Access denied"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Canned Response"
|
||||||
|
],
|
||||||
|
"operationId": "add-new-canned-response-to-account",
|
||||||
|
"summary": "Add a New Canned Response",
|
||||||
|
"description": "Add a new Canned Response to Account",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"userApiKey": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "data",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/canned_response_create_update_payload"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/canned_response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Access denied"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/accounts/{account_id}/canned_responses/{id}": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "#/parameters/account_id"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"patch": {
|
||||||
|
"tags": [
|
||||||
|
"Canned Response"
|
||||||
|
],
|
||||||
|
"operationId": "update-canned-response-in-account",
|
||||||
|
"summary": "Update Canned Response in Account",
|
||||||
|
"description": "Update a Canned Response in Account",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"userApiKey": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"required": true,
|
||||||
|
"description": "The ID of the canned response to be updated."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/canned_response_create_update_payload"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/canned_response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Agent not found"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Access denied"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Canned Response"
|
||||||
|
],
|
||||||
|
"operationId": "delete-canned-response-from-account",
|
||||||
|
"summary": "Remove a Canned Response from Account",
|
||||||
|
"description": "Remove a Canned Response from Account",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"userApiKey": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"required": true,
|
||||||
|
"description": "The ID of the canned response to be deleted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Canned Response not found"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Access denied"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/accounts/{account_id}/contacts": {
|
"/api/v1/accounts/{account_id}/contacts": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
@ -3443,7 +3604,15 @@
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "Array of date based conversation statistics",
|
"description": "Array of date based conversation statistics",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/report"
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3490,14 +3659,110 @@
|
||||||
"operationId": "list-all-conversation-statistics-summary",
|
"operationId": "list-all-conversation-statistics-summary",
|
||||||
"summary": "Get Account reports summary",
|
"summary": "Get Account reports summary",
|
||||||
"description": "Get Account reports summary for a specific type and date range",
|
"description": "Get Account reports summary for a specific type and date range",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/account_summary"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "reports not found"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Access denied"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v2/accounts/{account_id}/reports/conversations": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "#/parameters/account_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "type",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"account"
|
||||||
|
],
|
||||||
|
"required": true,
|
||||||
|
"description": "Type of report"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Reports"
|
||||||
|
],
|
||||||
|
"operationId": "get-account-conversation-metrics",
|
||||||
|
"summary": "Account Conversation Metrics",
|
||||||
|
"description": "Get conversation metrics for Account",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Object of account conversation metrics",
|
||||||
|
"properties": {
|
||||||
|
"open": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"unattended": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"unassigned": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "reports not found"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Access denied"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v2/accounts/{account_id}/reports/conversations/": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "#/parameters/account_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "type",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"agent"
|
||||||
|
],
|
||||||
|
"required": true,
|
||||||
|
"description": "Type of report"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "string",
|
||||||
|
"description": "The numeric ID of the user"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Reports"
|
||||||
|
],
|
||||||
|
"operationId": "get-agent-conversation-metrics",
|
||||||
|
"summary": "Agent Conversation Metrics",
|
||||||
|
"description": "Get conversation metrics for Agent",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Success",
|
"description": "Success",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "Array of date based conversation statistics",
|
"description": "Array of agent based conversation metrics",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/report"
|
"$ref": "#/definitions/agent_conversation_metrics"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3549,6 +3814,27 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"canned_response": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "ID of the canned response"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Message content for canned response"
|
||||||
|
},
|
||||||
|
"short_code": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Short Code for quick access of the canned response"
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Account Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -4196,6 +4482,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"canned_response_create_update_payload": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Message content for canned response"
|
||||||
|
},
|
||||||
|
"short_code": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Short Code for quick access of the canned response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"contact_create": {
|
"contact_create": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -4684,58 +4983,80 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"report": {
|
"account_summary": {
|
||||||
"type": "array",
|
"type": "object",
|
||||||
"description": "array of conversation count based on date",
|
"properties": {
|
||||||
"items": {
|
"avg_first_response_time": {
|
||||||
"allOf": [
|
"type": "string"
|
||||||
{
|
},
|
||||||
"type": "object",
|
"avg_resolution_time": {
|
||||||
"properties": {
|
"type": "string"
|
||||||
"avg_first_response_time": {
|
},
|
||||||
"type": "string"
|
"conversations_count": {
|
||||||
},
|
"type": "number"
|
||||||
"avg_resolution_time": {
|
},
|
||||||
"type": "string"
|
"incoming_messages_count": {
|
||||||
},
|
"type": "number"
|
||||||
"conversations_count": {
|
},
|
||||||
"type": "number"
|
"outgoing_messages_count": {
|
||||||
},
|
"type": "number"
|
||||||
"incoming_messages_count": {
|
},
|
||||||
"type": "number"
|
"resolutions_count": {
|
||||||
},
|
"type": "number"
|
||||||
"outgoing_messages_count": {
|
},
|
||||||
"type": "number"
|
"previous": {
|
||||||
},
|
"type": "object",
|
||||||
"resolutions_count": {
|
"properties": {
|
||||||
"type": "number"
|
"avg_first_response_time": {
|
||||||
},
|
"type": "string"
|
||||||
"previous": {
|
},
|
||||||
"type": "object",
|
"avg_resolution_time": {
|
||||||
"properties": {
|
"type": "string"
|
||||||
"avg_first_response_time": {
|
},
|
||||||
"type": "string"
|
"conversations_count": {
|
||||||
},
|
"type": "number"
|
||||||
"avg_resolution_time": {
|
},
|
||||||
"type": "string"
|
"incoming_messages_count": {
|
||||||
},
|
"type": "number"
|
||||||
"conversations_count": {
|
},
|
||||||
"type": "number"
|
"outgoing_messages_count": {
|
||||||
},
|
"type": "number"
|
||||||
"incoming_messages_count": {
|
},
|
||||||
"type": "number"
|
"resolutions_count": {
|
||||||
},
|
"type": "number"
|
||||||
"outgoing_messages_count": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"resolutions_count": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"agent_conversation_metrics": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"user": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metric": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"open": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"unattended": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -4894,6 +5215,7 @@
|
||||||
"tags": [
|
"tags": [
|
||||||
"Account AgentBots",
|
"Account AgentBots",
|
||||||
"Agent",
|
"Agent",
|
||||||
|
"Canned Response",
|
||||||
"Contact",
|
"Contact",
|
||||||
"Conversation",
|
"Conversation",
|
||||||
"Conversation Assignment",
|
"Conversation Assignment",
|
||||||
|
|
Loading…
Reference in a new issue