Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
924d3ef2df | ||
|
|
4ef06530e5 | ||
|
|
3f10e89dba | ||
|
|
184325dcae | ||
|
|
ce2a4f5c34 | ||
|
|
7c2f4ec334 | ||
|
|
a69894e0fc | ||
|
|
6f36a02519 | ||
|
|
54e697ec51 | ||
|
|
ee044e8602 | ||
|
|
3b7421221c | ||
|
|
bc9327c09f | ||
|
|
ac71dacc61 | ||
|
|
072c607062 | ||
|
|
4b67066027 | ||
|
|
08834b6016 | ||
|
|
cbdc5b1ab0 | ||
|
|
25c6adbd07 | ||
|
|
d552b41fac | ||
|
|
a315321c4f | ||
|
|
2e768e2042 | ||
|
|
bd481ad314 | ||
|
|
b90737ee5d | ||
|
|
6e57e76358 | ||
|
|
75998945a5 | ||
|
|
cc80e4122a | ||
|
|
8aa0a347d6 | ||
|
|
2a34ceb66b | ||
|
|
db30a05b19 | ||
|
|
73fcb91ca3 | ||
|
|
bcd5633d2a | ||
|
|
1a272e7a18 | ||
|
|
7d4374a3bf | ||
|
|
2617d9b2b7 | ||
|
|
375d216d12 |
+16
-56
@@ -148,7 +148,7 @@ GEM
|
|||||||
barnes (0.0.9)
|
barnes (0.0.9)
|
||||||
multi_json (~> 1)
|
multi_json (~> 1)
|
||||||
statsd-ruby (~> 1.1)
|
statsd-ruby (~> 1.1)
|
||||||
base64 (0.2.0)
|
base64 (0.1.1)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
bigdecimal (3.1.8)
|
bigdecimal (3.1.8)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
@@ -242,8 +242,9 @@ GEM
|
|||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
faker (3.2.0)
|
faker (3.2.0)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (2.9.0)
|
faraday (2.7.4)
|
||||||
faraday-net_http (>= 2.0, < 3.2)
|
faraday-net_http (>= 2.0, < 3.1)
|
||||||
|
ruby2_keywords (>= 0.0.4)
|
||||||
faraday-follow_redirects (0.3.0)
|
faraday-follow_redirects (0.3.0)
|
||||||
faraday (>= 1, < 3)
|
faraday (>= 1, < 3)
|
||||||
faraday-mashify (0.1.1)
|
faraday-mashify (0.1.1)
|
||||||
@@ -251,8 +252,7 @@ GEM
|
|||||||
hashie
|
hashie
|
||||||
faraday-multipart (1.0.4)
|
faraday-multipart (1.0.4)
|
||||||
multipart-post (~> 2)
|
multipart-post (~> 2)
|
||||||
faraday-net_http (3.1.0)
|
faraday-net_http (3.0.2)
|
||||||
net-http
|
|
||||||
faraday-net_http_persistent (2.1.0)
|
faraday-net_http_persistent (2.1.0)
|
||||||
faraday (~> 2.5)
|
faraday (~> 2.5)
|
||||||
net-http-persistent (~> 4.0)
|
net-http-persistent (~> 4.0)
|
||||||
@@ -321,10 +321,7 @@ GEM
|
|||||||
google-cloud-translate-v3 (0.6.0)
|
google-cloud-translate-v3 (0.6.0)
|
||||||
gapic-common (>= 0.17.1, < 2.a)
|
gapic-common (>= 0.17.1, < 2.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-protobuf (3.25.3)
|
|
||||||
google-protobuf (3.25.3-arm64-darwin)
|
google-protobuf (3.25.3-arm64-darwin)
|
||||||
google-protobuf (3.25.3-x86_64-darwin)
|
|
||||||
google-protobuf (3.25.3-x86_64-linux)
|
|
||||||
googleapis-common-protos (1.4.0)
|
googleapis-common-protos (1.4.0)
|
||||||
google-protobuf (~> 3.14)
|
google-protobuf (~> 3.14)
|
||||||
googleapis-common-protos-types (~> 1.2)
|
googleapis-common-protos-types (~> 1.2)
|
||||||
@@ -340,16 +337,7 @@ GEM
|
|||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
groupdate (6.2.1)
|
groupdate (6.2.1)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
grpc (1.62.0)
|
grpc (1.63.0-arm64-darwin)
|
||||||
google-protobuf (~> 3.25)
|
|
||||||
googleapis-common-protos-types (~> 1.0)
|
|
||||||
grpc (1.62.0-arm64-darwin)
|
|
||||||
google-protobuf (~> 3.25)
|
|
||||||
googleapis-common-protos-types (~> 1.0)
|
|
||||||
grpc (1.62.0-x86_64-darwin)
|
|
||||||
google-protobuf (~> 3.25)
|
|
||||||
googleapis-common-protos-types (~> 1.0)
|
|
||||||
grpc (1.62.0-x86_64-linux)
|
|
||||||
google-protobuf (~> 3.25)
|
google-protobuf (~> 3.25)
|
||||||
googleapis-common-protos-types (~> 1.0)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
haikunator (1.1.1)
|
haikunator (1.1.1)
|
||||||
@@ -373,7 +361,7 @@ GEM
|
|||||||
mini_mime (>= 1.0.0)
|
mini_mime (>= 1.0.0)
|
||||||
multi_xml (>= 0.5.2)
|
multi_xml (>= 0.5.2)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
i18n (1.14.5)
|
i18n (1.14.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
image_processing (1.12.2)
|
image_processing (1.12.2)
|
||||||
mini_magick (>= 4.9.5, < 5)
|
mini_magick (>= 4.9.5, < 5)
|
||||||
@@ -397,8 +385,7 @@ GEM
|
|||||||
hana (~> 1.3)
|
hana (~> 1.3)
|
||||||
regexp_parser (~> 2.0)
|
regexp_parser (~> 2.0)
|
||||||
uri_template (~> 0.7)
|
uri_template (~> 0.7)
|
||||||
jwt (2.8.1)
|
jwt (2.7.0)
|
||||||
base64
|
|
||||||
kaminari (1.2.2)
|
kaminari (1.2.2)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
kaminari-actionview (= 1.2.2)
|
kaminari-actionview (= 1.2.2)
|
||||||
@@ -422,15 +409,8 @@ GEM
|
|||||||
letter_opener (1.8.1)
|
letter_opener (1.8.1)
|
||||||
launchy (>= 2.2, < 3)
|
launchy (>= 2.2, < 3)
|
||||||
libdatadog (7.0.0.1.0)
|
libdatadog (7.0.0.1.0)
|
||||||
libdatadog (7.0.0.1.0-x86_64-linux)
|
|
||||||
libddwaf (1.14.0.0.0)
|
|
||||||
ffi (~> 1.0)
|
|
||||||
libddwaf (1.14.0.0.0-arm64-darwin)
|
libddwaf (1.14.0.0.0-arm64-darwin)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
libddwaf (1.14.0.0.0-x86_64-darwin)
|
|
||||||
ffi (~> 1.0)
|
|
||||||
libddwaf (1.14.0.0.0-x86_64-linux)
|
|
||||||
ffi (~> 1.0)
|
|
||||||
line-bot-api (1.28.0)
|
line-bot-api (1.28.0)
|
||||||
liquid (5.4.0)
|
liquid (5.4.0)
|
||||||
listen (3.8.0)
|
listen (3.8.0)
|
||||||
@@ -459,13 +439,12 @@ GEM
|
|||||||
meta_request (0.8.2)
|
meta_request (0.8.2)
|
||||||
rack-contrib (>= 1.1, < 3)
|
rack-contrib (>= 1.1, < 3)
|
||||||
railties (>= 3.0.0, < 8)
|
railties (>= 3.0.0, < 8)
|
||||||
method_source (1.1.0)
|
method_source (1.0.0)
|
||||||
mime-types (3.4.1)
|
mime-types (3.4.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2023.0218.1)
|
mime-types-data (3.2023.0218.1)
|
||||||
mini_magick (4.12.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.7)
|
|
||||||
minitest (5.24.1)
|
minitest (5.24.1)
|
||||||
mock_redis (0.36.0)
|
mock_redis (0.36.0)
|
||||||
ruby2_keywords
|
ruby2_keywords
|
||||||
@@ -475,8 +454,6 @@ GEM
|
|||||||
multipart-post (2.3.0)
|
multipart-post (2.3.0)
|
||||||
neighbor (0.2.3)
|
neighbor (0.2.3)
|
||||||
activerecord (>= 5.2)
|
activerecord (>= 5.2)
|
||||||
net-http (0.4.1)
|
|
||||||
uri
|
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.4.12)
|
net-imap (0.4.12)
|
||||||
@@ -495,15 +472,8 @@ GEM
|
|||||||
newrelic_rpm (9.6.0)
|
newrelic_rpm (9.6.0)
|
||||||
base64
|
base64
|
||||||
nio4r (2.7.3)
|
nio4r (2.7.3)
|
||||||
nokogiri (1.16.6)
|
|
||||||
mini_portile2 (~> 2.8.2)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.16.6-arm64-darwin)
|
nokogiri (1.16.6-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.6-x86_64-darwin)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.16.6-x86_64-linux)
|
|
||||||
racc (~> 1.4)
|
|
||||||
oauth (1.1.0)
|
oauth (1.1.0)
|
||||||
oauth-tty (~> 1.0, >= 1.0.1)
|
oauth-tty (~> 1.0, >= 1.0.1)
|
||||||
snaky_hash (~> 2.0)
|
snaky_hash (~> 2.0)
|
||||||
@@ -566,8 +536,7 @@ GEM
|
|||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
rack-mini-profiler (3.2.0)
|
rack-mini-profiler (3.2.0)
|
||||||
rack (>= 1.2.0)
|
rack (>= 1.2.0)
|
||||||
rack-protection (3.2.0)
|
rack-protection (3.1.0)
|
||||||
base64 (>= 0.1.0)
|
|
||||||
rack (~> 2.2, >= 2.2.4)
|
rack (~> 2.2, >= 2.2.4)
|
||||||
rack-proxy (0.7.6)
|
rack-proxy (0.7.6)
|
||||||
rack
|
rack
|
||||||
@@ -603,7 +572,7 @@ GEM
|
|||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (~> 2.5)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.2.1)
|
rake (13.1.0)
|
||||||
rb-fsevent (0.11.2)
|
rb-fsevent (0.11.2)
|
||||||
rb-inotify (0.10.1)
|
rb-inotify (0.10.1)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
@@ -633,8 +602,8 @@ GEM
|
|||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
reverse_markdown (2.1.1)
|
reverse_markdown (2.1.1)
|
||||||
nokogiri
|
nokogiri
|
||||||
rexml (3.2.8)
|
rexml (3.3.1)
|
||||||
strscan (>= 3.0.9)
|
strscan
|
||||||
rspec-core (3.13.0)
|
rspec-core (3.13.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-expectations (3.13.1)
|
rspec-expectations (3.13.1)
|
||||||
@@ -768,7 +737,7 @@ GEM
|
|||||||
strscan (3.1.0)
|
strscan (3.1.0)
|
||||||
telephone_number (1.4.20)
|
telephone_number (1.4.20)
|
||||||
test-prof (1.2.1)
|
test-prof (1.2.1)
|
||||||
thor (1.3.1)
|
thor (1.3.0)
|
||||||
tilt (2.3.0)
|
tilt (2.3.0)
|
||||||
time_diff (0.3.0)
|
time_diff (0.3.0)
|
||||||
activesupport
|
activesupport
|
||||||
@@ -793,12 +762,11 @@ GEM
|
|||||||
unf_ext (0.0.8.2)
|
unf_ext (0.0.8.2)
|
||||||
unicode-display_width (2.4.2)
|
unicode-display_width (2.4.2)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.16.0)
|
||||||
uri (0.13.0)
|
|
||||||
uri_template (0.7.0)
|
uri_template (0.7.0)
|
||||||
valid_email2 (4.0.6)
|
valid_email2 (4.0.6)
|
||||||
activemodel (>= 3.2)
|
activemodel (>= 3.2)
|
||||||
mail (~> 2.5)
|
mail (~> 2.5)
|
||||||
version_gem (1.1.4)
|
version_gem (1.1.3)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.2.1)
|
web-console (4.2.1)
|
||||||
@@ -829,15 +797,7 @@ GEM
|
|||||||
zeitwerk (2.6.16)
|
zeitwerk (2.6.16)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-20
|
arm64-darwin-23
|
||||||
arm64-darwin-21
|
|
||||||
arm64-darwin-22
|
|
||||||
ruby
|
|
||||||
x86_64-darwin-18
|
|
||||||
x86_64-darwin-20
|
|
||||||
x86_64-darwin-21
|
|
||||||
x86_64-darwin-22
|
|
||||||
x86_64-linux
|
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
active_record_query_trace
|
active_record_query_trace
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
class V2::BaseConversationReportBuilder
|
||||||
|
pattr_initialize :account, :params
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
AVG_METRICS = %w[avg_first_response_time avg_resolution_time reply_time].freeze
|
||||||
|
COUNT_METRICS = %w[
|
||||||
|
conversations_count
|
||||||
|
incoming_messages_count
|
||||||
|
outgoing_messages_count
|
||||||
|
resolutions_count
|
||||||
|
bot_resolutions_count
|
||||||
|
bot_handoffs_count
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
def builder_class(metric)
|
||||||
|
case metric
|
||||||
|
when *AVG_METRICS
|
||||||
|
V2::Reports::Timeseries::AverageReportBuilder
|
||||||
|
when *COUNT_METRICS
|
||||||
|
V2::Reports::Timeseries::CountReportBuilder
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_invalid_metric
|
||||||
|
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
|
||||||
|
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
class V2::ConversationMetricBuilder < V2::BaseConversationReportBuilder
|
||||||
|
def summary
|
||||||
|
{
|
||||||
|
conversations_count: count('conversations_count'),
|
||||||
|
incoming_messages_count: count('incoming_messages_count'),
|
||||||
|
outgoing_messages_count: count('outgoing_messages_count'),
|
||||||
|
avg_first_response_time: count('avg_first_response_time'),
|
||||||
|
avg_resolution_time: count('avg_resolution_time'),
|
||||||
|
resolutions_count: count('resolutions_count'),
|
||||||
|
reply_time: count('reply_time')
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def bot_summary
|
||||||
|
{
|
||||||
|
bot_resolutions_count: count('bot_resolutions_count'),
|
||||||
|
bot_handoffs_count: count('bot_handoffs_count')
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def count(metric)
|
||||||
|
builder_class(metric).new(account, builder_params(metric)).aggregate_value
|
||||||
|
end
|
||||||
|
|
||||||
|
def builder_params(metric)
|
||||||
|
params.merge({ metric: metric })
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
class V2::ConversationReportBuilder < V2::BaseConversationReportBuilder
|
||||||
|
include DateRangeHelper
|
||||||
|
|
||||||
|
pattr_initialize :account, :params
|
||||||
|
|
||||||
|
def timeseries
|
||||||
|
perform_action(:timeseries)
|
||||||
|
end
|
||||||
|
|
||||||
|
def aggregate_value
|
||||||
|
perform_action(:aggregate_value)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def perform_action(method_name)
|
||||||
|
return builder.new(account, params).public_send(method_name) if builder.present?
|
||||||
|
|
||||||
|
log_invalid_metric
|
||||||
|
end
|
||||||
|
|
||||||
|
def builder
|
||||||
|
builder_class(params[:metric])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,6 +3,7 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
|||||||
|
|
||||||
def build
|
def build
|
||||||
set_grouped_conversations_count
|
set_grouped_conversations_count
|
||||||
|
set_grouped_resolved_conversations_count
|
||||||
set_grouped_avg_reply_time
|
set_grouped_avg_reply_time
|
||||||
set_grouped_avg_first_response_time
|
set_grouped_avg_first_response_time
|
||||||
set_grouped_avg_resolution_time
|
set_grouped_avg_resolution_time
|
||||||
@@ -27,6 +28,15 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
|||||||
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_grouped_resolved_conversations_count
|
||||||
|
@grouped_resolved_conversations_count = reporting_events
|
||||||
|
.where(name: 'conversation_resolved')
|
||||||
|
.select(group_by_key, :conversation_id)
|
||||||
|
.distinct
|
||||||
|
.group(group_by_key)
|
||||||
|
.count(:conversation_id)
|
||||||
|
end
|
||||||
|
|
||||||
def group_by_key
|
def group_by_key
|
||||||
:user_id
|
:user_id
|
||||||
end
|
end
|
||||||
@@ -37,12 +47,14 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
|||||||
|
|
||||||
def prepare_report
|
def prepare_report
|
||||||
account.account_users.each_with_object([]) do |account_user, arr|
|
account.account_users.each_with_object([]) do |account_user, arr|
|
||||||
|
user_id = account_user.user_id
|
||||||
arr << {
|
arr << {
|
||||||
id: account_user.user_id,
|
id: user_id,
|
||||||
conversations_count: @grouped_conversations_count[account_user.user_id],
|
conversations_count: @grouped_conversations_count[user_id],
|
||||||
avg_resolution_time: @grouped_avg_resolution_time[account_user.user_id],
|
resolved_conversations_count: @grouped_resolved_conversations_count[user_id],
|
||||||
avg_first_response_time: @grouped_avg_first_response_time[account_user.user_id],
|
avg_resolution_time: @grouped_avg_resolution_time[user_id],
|
||||||
avg_reply_time: @grouped_avg_reply_time[account_user.user_id]
|
avg_first_response_time: @grouped_avg_first_response_time[user_id],
|
||||||
|
avg_reply_time: @grouped_avg_reply_time[user_id]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||||
|
pattr_initialize [:account!, :params!]
|
||||||
|
|
||||||
|
def build
|
||||||
|
set_grouped_conversations_count
|
||||||
|
set_grouped_resolved_conversations_count
|
||||||
|
set_grouped_avg_reply_time
|
||||||
|
set_grouped_avg_first_response_time
|
||||||
|
set_grouped_avg_resolution_time
|
||||||
|
prepare_report
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_grouped_conversations_count
|
||||||
|
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('inbox_id').count
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_grouped_avg_resolution_time
|
||||||
|
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_grouped_avg_first_response_time
|
||||||
|
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_grouped_avg_reply_time
|
||||||
|
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_grouped_resolved_conversations_count
|
||||||
|
@grouped_resolved_conversations_count = reporting_events
|
||||||
|
.where(name: 'conversation_resolved')
|
||||||
|
.select(group_by_key, :conversation_id)
|
||||||
|
.distinct
|
||||||
|
.group(group_by_key)
|
||||||
|
.count(:conversation_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_by_key
|
||||||
|
:inbox_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def reporting_events
|
||||||
|
@reporting_events ||= Current.account.reporting_events.where(created_at: range)
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_report
|
||||||
|
account.inboxes.each_with_object([]) do |inbox, arr|
|
||||||
|
inbox_id = inbox.id
|
||||||
|
arr << {
|
||||||
|
id: inbox_id,
|
||||||
|
conversations_count: @grouped_conversations_count[inbox_id],
|
||||||
|
resolved_conversations_count: @grouped_resolved_conversations_count[inbox_id],
|
||||||
|
avg_resolution_time: @grouped_avg_resolution_time[inbox_id],
|
||||||
|
avg_first_response_time: @grouped_avg_first_response_time[inbox_id],
|
||||||
|
avg_reply_time: @grouped_avg_reply_time[inbox_id]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def average_value_key
|
||||||
|
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,6 +3,7 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
|||||||
|
|
||||||
def build
|
def build
|
||||||
set_grouped_conversations_count
|
set_grouped_conversations_count
|
||||||
|
set_grouped_resolved_conversations_count
|
||||||
set_grouped_avg_reply_time
|
set_grouped_avg_reply_time
|
||||||
set_grouped_avg_first_response_time
|
set_grouped_avg_first_response_time
|
||||||
set_grouped_avg_resolution_time
|
set_grouped_avg_resolution_time
|
||||||
@@ -27,6 +28,15 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
|||||||
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_grouped_resolved_conversations_count
|
||||||
|
@grouped_resolved_conversations_count = reporting_events
|
||||||
|
.where(name: 'conversation_resolved')
|
||||||
|
.select(group_by_key, :conversation_id)
|
||||||
|
.distinct
|
||||||
|
.group(group_by_key)
|
||||||
|
.count(:conversation_id)
|
||||||
|
end
|
||||||
|
|
||||||
def reporting_events
|
def reporting_events
|
||||||
@reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation)
|
@reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation)
|
||||||
end
|
end
|
||||||
@@ -40,6 +50,7 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
|||||||
arr << {
|
arr << {
|
||||||
id: team.id,
|
id: team.id,
|
||||||
conversations_count: @grouped_conversations_count[team.id],
|
conversations_count: @grouped_conversations_count[team.id],
|
||||||
|
resolved_conversations_count: @grouped_resolved_conversations_count[team.id],
|
||||||
avg_resolution_time: @grouped_avg_resolution_time[team.id],
|
avg_resolution_time: @grouped_avg_resolution_time[team.id],
|
||||||
avg_first_response_time: @grouped_avg_first_response_time[team.id],
|
avg_first_response_time: @grouped_avg_first_response_time[team.id],
|
||||||
avg_reply_time: @grouped_avg_reply_time[team.id]
|
avg_reply_time: @grouped_avg_reply_time[team.id]
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
class Api::V2::Accounts::LiveReportsController < Api::V1::Accounts::BaseController
|
||||||
|
before_action :load_conversations, only: [:conversation_metrics, :grouped_conversation_metrics]
|
||||||
|
before_action :set_group_scope, only: [:grouped_conversation_metrics]
|
||||||
|
|
||||||
|
def conversation_metrics
|
||||||
|
render json: {
|
||||||
|
open: @conversations.open.count,
|
||||||
|
unattended: @conversations.open.unattended.count,
|
||||||
|
unassigned: @conversations.open.unassigned.count
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def grouped_conversation_metrics
|
||||||
|
count_by_group = @conversations.open.group(@group_scope).count
|
||||||
|
unattended_by_group = @conversations.open.unattended.group(@group_scope).count
|
||||||
|
unassigned_by_group = @conversations.open.unassigned.group(@group_scope).count
|
||||||
|
|
||||||
|
group_metrics = count_by_group.map do |group_id, count|
|
||||||
|
metric = {
|
||||||
|
open: count,
|
||||||
|
unattended: unattended_by_group[group_id] || 0,
|
||||||
|
unassigned: unassigned_by_group[group_id] || 0
|
||||||
|
}
|
||||||
|
metric[@group_scope] = group_id
|
||||||
|
metric
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: group_metrics
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_group_scope
|
||||||
|
render json: { error: 'invalid group_by' }, status: :unprocessable_entity and return unless %w[
|
||||||
|
team_id
|
||||||
|
assignee_id
|
||||||
|
].include?(permitted_params[:group_by])
|
||||||
|
|
||||||
|
@group_scope = permitted_params[:group_by]
|
||||||
|
end
|
||||||
|
|
||||||
|
def team
|
||||||
|
return unless permitted_params[:team_id]
|
||||||
|
|
||||||
|
@team ||= Current.account.teams.find(permitted_params[:team_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_conversations
|
||||||
|
scope = Current.account.conversations
|
||||||
|
scope = scope.where(team_id: team.id) if team.present?
|
||||||
|
@conversations = scope
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:team_id, :group_by)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
|
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :prepare_builder_params, only: [:agent, :team]
|
before_action :prepare_builder_params, only: [:agent, :team, :inbox]
|
||||||
|
|
||||||
def agent
|
def agent
|
||||||
render_report_with(V2::Reports::AgentSummaryBuilder)
|
render_report_with(V2::Reports::AgentSummaryBuilder)
|
||||||
@@ -10,6 +10,10 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
|
|||||||
render_report_with(V2::Reports::TeamSummaryBuilder)
|
render_report_with(V2::Reports::TeamSummaryBuilder)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def inbox
|
||||||
|
render_report_with(V2::Reports::InboxSummaryBuilder)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def check_authorization
|
def check_authorization
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ module Api::V2::Accounts::HeatmapHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def since_timestamp(date)
|
def since_timestamp(date)
|
||||||
(date - 6.days).to_i.to_s
|
number_of_days = params[:days_before].present? ? params[:days_before].to_i.days : 6.days
|
||||||
|
(date - number_of_days).to_i.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def until_timestamp(date)
|
def until_timestamp(date)
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/* global axios */
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class LiveReportsAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('live_reports', { accountScoped: true, apiVersion: 'v2' });
|
||||||
|
}
|
||||||
|
|
||||||
|
getConversationMetric(params = {}) {
|
||||||
|
return axios.get(`${this.url}/conversation_metrics`, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
getGroupedConversations({ groupBy } = { groupBy: 'assignee_id' }) {
|
||||||
|
return axios.get(`${this.url}/grouped_conversation_metrics`, {
|
||||||
|
params: { group_by: groupBy },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new LiveReportsAPI();
|
||||||
@@ -61,9 +61,9 @@ class ReportsAPI extends ApiClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getConversationTrafficCSV() {
|
getConversationTrafficCSV({ daysBefore = 6 } = {}) {
|
||||||
return axios.get(`${this.url}/conversation_traffic`, {
|
return axios.get(`${this.url}/conversation_traffic`, {
|
||||||
params: { timezone_offset: getTimeOffset() },
|
params: { timezone_offset: getTimeOffset(), days_before: daysBefore },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/* global axios */
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class SummaryReportsAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('summary_reports', { accountScoped: true, apiVersion: 'v2' });
|
||||||
|
}
|
||||||
|
|
||||||
|
getTeamReports({ since, until, businessHours } = {}) {
|
||||||
|
return axios.get(`${this.url}/team`, {
|
||||||
|
params: {
|
||||||
|
since,
|
||||||
|
until,
|
||||||
|
business_hours: businessHours,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAgentReports({ since, until, businessHours } = {}) {
|
||||||
|
return axios.get(`${this.url}/agent`, {
|
||||||
|
params: {
|
||||||
|
since,
|
||||||
|
until,
|
||||||
|
business_hours: businessHours,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getInboxReports({ since, until, businessHours } = {}) {
|
||||||
|
return axios.get(`${this.url}/inbox`, {
|
||||||
|
params: {
|
||||||
|
since,
|
||||||
|
until,
|
||||||
|
business_hours: businessHours,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new SummaryReportsAPI();
|
||||||
@@ -494,6 +494,17 @@
|
|||||||
"STATUS": "Status"
|
"STATUS": "Status"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"TEAM_CONVERSATIONS": {
|
||||||
|
"HEADER": "Conversations by teams",
|
||||||
|
"LOADING_MESSAGE": "Loading team metrics...",
|
||||||
|
"NO_AGENTS": "There is no data available",
|
||||||
|
"TABLE_HEADER": {
|
||||||
|
"AGENT": "Team",
|
||||||
|
"OPEN": "open",
|
||||||
|
"UNATTENDED": "Unattended",
|
||||||
|
"STATUS": "Status"
|
||||||
|
}
|
||||||
|
},
|
||||||
"AGENT_STATUS": {
|
"AGENT_STATUS": {
|
||||||
"HEADER": "Agent status",
|
"HEADER": "Agent status",
|
||||||
"ONLINE": "Online",
|
"ONLINE": "Online",
|
||||||
|
|||||||
@@ -1,19 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<woot-reports
|
<div class="column overflow-auto">
|
||||||
key="agent-reports"
|
<metric-card :is-live="false" header="All agents overview">
|
||||||
type="agent"
|
<woot-summary-reports
|
||||||
getter-key="agents/getAgents"
|
key="agent-summary-reports"
|
||||||
action-key="agents/get"
|
class="!p-0"
|
||||||
:download-button-label="$t('REPORT.DOWNLOAD_AGENT_REPORTS')"
|
type="agent"
|
||||||
/>
|
getter-key="agents/getAgents"
|
||||||
|
attribute-key="team_id"
|
||||||
|
action-key="summaryReports/fetchAgentSummaryReports"
|
||||||
|
summary-key="summaryReports/getAgentSummaryReports"
|
||||||
|
:download-button-label="$t('REPORT.DOWNLOAD_AGENT_REPORTS')"
|
||||||
|
/>
|
||||||
|
</metric-card>
|
||||||
|
<metric-card
|
||||||
|
:is-live="false"
|
||||||
|
header="Agent-wise reports"
|
||||||
|
class="overflow-visible"
|
||||||
|
>
|
||||||
|
<woot-reports
|
||||||
|
key="agent-reports"
|
||||||
|
:show-download-button="false"
|
||||||
|
class="!p-0"
|
||||||
|
type="agent"
|
||||||
|
getter-key="agents/getAgents"
|
||||||
|
action-key="agents/get"
|
||||||
|
/>
|
||||||
|
</metric-card>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import WootSummaryReports from './components/WootSummaryReports.vue';
|
||||||
|
import MetricCard from './components/overview/MetricCard.vue';
|
||||||
import WootReports from './components/WootReports.vue';
|
import WootReports from './components/WootReports.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
WootReports,
|
WootReports,
|
||||||
|
MetricCard,
|
||||||
|
WootSummaryReports,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,19 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<woot-reports
|
<div class="column overflow-auto">
|
||||||
key="inbox-reports"
|
<metric-card :is-live="false" header="All inboxes overview">
|
||||||
type="inbox"
|
<woot-summary-reports
|
||||||
getter-key="inboxes/getInboxes"
|
key="inbox-view-reports"
|
||||||
action-key="inboxes/get"
|
class="!p-0"
|
||||||
:download-button-label="$t('INBOX_REPORTS.DOWNLOAD_INBOX_REPORTS')"
|
type="inbox"
|
||||||
/>
|
getter-key="inboxes/getInboxes"
|
||||||
|
action-key="summaryReports/fetchInboxSummaryReports"
|
||||||
|
summary-key="summaryReports/getInboxSummaryReports"
|
||||||
|
:download-button-label="$t('INBOX_REPORTS.DOWNLOAD_INBOX_REPORTS')"
|
||||||
|
/>
|
||||||
|
</metric-card>
|
||||||
|
<metric-card
|
||||||
|
:is-live="false"
|
||||||
|
header="Inbox-wise reports"
|
||||||
|
class="overflow-visible"
|
||||||
|
>
|
||||||
|
<woot-reports
|
||||||
|
key="inbox-reports"
|
||||||
|
:show-download-button="false"
|
||||||
|
class="!p-0"
|
||||||
|
type="inbox"
|
||||||
|
getter-key="inboxes/getInboxes"
|
||||||
|
action-key="inboxes/get"
|
||||||
|
/>
|
||||||
|
</metric-card>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import WootSummaryReports from './components/WootSummaryReports.vue';
|
||||||
|
import MetricCard from './components/overview/MetricCard.vue';
|
||||||
import WootReports from './components/WootReports.vue';
|
import WootReports from './components/WootReports.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
WootReports,
|
WootReports,
|
||||||
|
MetricCard,
|
||||||
|
WootSummaryReports,
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('agents/get');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,30 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex-1 overflow-auto p-4">
|
<div class="flex-1 overflow-auto p-4">
|
||||||
<div class="flex flex-col md:flex-row items-center">
|
<div class="flex">
|
||||||
<div
|
<open-conversations />
|
||||||
class="flex-1 w-full max-w-full md:w-[65%] md:max-w-[65%] conversation-metric"
|
|
||||||
>
|
<div class="flex-1 w-full max-w-full md:w-[35%] md:max-w-[35%] flex">
|
||||||
<metric-card
|
<metric-card
|
||||||
:header="$t('OVERVIEW_REPORTS.ACCOUNT_CONVERSATIONS.HEADER')"
|
:header="$t('OVERVIEW_REPORTS.AGENT_STATUS.HEADER')"
|
||||||
:is-loading="uiFlags.isFetchingAccountConversationMetric"
|
class="flex-1"
|
||||||
:loading-message="
|
|
||||||
$t('OVERVIEW_REPORTS.ACCOUNT_CONVERSATIONS.LOADING_MESSAGE')
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
v-for="(metric, name, index) in conversationMetrics"
|
|
||||||
:key="index"
|
|
||||||
class="metric-content flex-1 min-w-0"
|
|
||||||
>
|
|
||||||
<h3 class="heading">
|
|
||||||
{{ name }}
|
|
||||||
</h3>
|
|
||||||
<p class="metric">{{ metric }}</p>
|
|
||||||
</div>
|
|
||||||
</metric-card>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 w-full max-w-full md:w-[35%] md:max-w-[35%]">
|
|
||||||
<metric-card :header="$t('OVERVIEW_REPORTS.AGENT_STATUS.HEADER')">
|
|
||||||
<div
|
<div
|
||||||
v-for="(metric, name, index) in agentStatusMetrics"
|
v-for="(metric, name, index) in agentStatusMetrics"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -41,6 +24,18 @@
|
|||||||
<div class="max-w-full flex flex-wrap flex-row ml-auto mr-auto">
|
<div class="max-w-full flex flex-wrap flex-row ml-auto mr-auto">
|
||||||
<metric-card :header="$t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.HEADER')">
|
<metric-card :header="$t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.HEADER')">
|
||||||
<template #control>
|
<template #control>
|
||||||
|
<multiselect-dropdown
|
||||||
|
class="!mb-0 !w-1/3"
|
||||||
|
:options="dayFilterOptions"
|
||||||
|
:selected-item="selectedDayFilter"
|
||||||
|
multiselector-title=""
|
||||||
|
multiselector-placeholder="Date filter"
|
||||||
|
no-search-result="No filter found"
|
||||||
|
input-placeholder="Search for a filter"
|
||||||
|
:is-filter="true"
|
||||||
|
:has-thumbnail="false"
|
||||||
|
@click="onSelectDateFilter"
|
||||||
|
/>
|
||||||
<woot-button
|
<woot-button
|
||||||
icon="arrow-download"
|
icon="arrow-download"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -52,6 +47,7 @@
|
|||||||
</woot-button>
|
</woot-button>
|
||||||
</template>
|
</template>
|
||||||
<report-heatmap
|
<report-heatmap
|
||||||
|
:selected-day-filter="selectedDayFilter"
|
||||||
:heat-data="accountConversationHeatmap"
|
:heat-data="accountConversationHeatmap"
|
||||||
:is-loading="uiFlags.isFetchingAccountConversationsHeatmap"
|
:is-loading="uiFlags.isFetchingAccountConversationsHeatmap"
|
||||||
/>
|
/>
|
||||||
@@ -64,7 +60,16 @@
|
|||||||
:agent-metrics="agentConversationMetric"
|
:agent-metrics="agentConversationMetric"
|
||||||
:page-index="pageIndex"
|
:page-index="pageIndex"
|
||||||
:is-loading="uiFlags.isFetchingAgentConversationMetric"
|
:is-loading="uiFlags.isFetchingAgentConversationMetric"
|
||||||
@page-change="onPageNumberChange"
|
/>
|
||||||
|
</metric-card>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<metric-card :header="$t('OVERVIEW_REPORTS.TEAM_CONVERSATIONS.HEADER')">
|
||||||
|
<team-table
|
||||||
|
:teams="teams"
|
||||||
|
:team-metrics="teamConversationMetric"
|
||||||
|
:page-index="pageIndex"
|
||||||
|
:is-loading="uiFlags.isFetchingTeamConversationMetric"
|
||||||
/>
|
/>
|
||||||
</metric-card>
|
</metric-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,34 +77,54 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import OpenConversations from './components/LiveReports/OpenConversations.vue';
|
||||||
import AgentTable from './components/overview/AgentTable.vue';
|
import AgentTable from './components/overview/AgentTable.vue';
|
||||||
|
import TeamTable from './components/overview/TeamTable.vue';
|
||||||
import MetricCard from './components/overview/MetricCard.vue';
|
import MetricCard from './components/overview/MetricCard.vue';
|
||||||
import { OVERVIEW_METRICS } from './constants';
|
|
||||||
import ReportHeatmap from './components/Heatmap.vue';
|
import ReportHeatmap from './components/Heatmap.vue';
|
||||||
|
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||||
|
|
||||||
import endOfDay from 'date-fns/endOfDay';
|
import endOfDay from 'date-fns/endOfDay';
|
||||||
import getUnixTime from 'date-fns/getUnixTime';
|
import getUnixTime from 'date-fns/getUnixTime';
|
||||||
import startOfDay from 'date-fns/startOfDay';
|
import startOfDay from 'date-fns/startOfDay';
|
||||||
import subDays from 'date-fns/subDays';
|
import subDays from 'date-fns/subDays';
|
||||||
|
import { OVERVIEW_METRICS } from './constants';
|
||||||
|
|
||||||
|
const dayFilterOptions = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Last 7 days',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Last 30 days',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'LiveReports',
|
name: 'LiveReports',
|
||||||
components: {
|
components: {
|
||||||
|
OpenConversations,
|
||||||
|
TeamTable,
|
||||||
AgentTable,
|
AgentTable,
|
||||||
MetricCard,
|
MetricCard,
|
||||||
ReportHeatmap,
|
ReportHeatmap,
|
||||||
|
MultiselectDropdown,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
pageIndex: 1,
|
pageIndex: 1,
|
||||||
|
dayFilterOptions: dayFilterOptions,
|
||||||
|
selectedDayFilter: dayFilterOptions[0],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
agentStatus: 'agents/getAgentStatus',
|
agentStatus: 'agents/getAgentStatus',
|
||||||
agents: 'agents/getAgents',
|
agents: 'agents/getAgents',
|
||||||
accountConversationMetric: 'getAccountConversationMetric',
|
teams: 'teams/getTeams',
|
||||||
agentConversationMetric: 'getAgentConversationMetric',
|
agentConversationMetric: 'getAgentConversationMetric',
|
||||||
|
teamConversationMetric: 'getTeamConversationMetric',
|
||||||
accountConversationHeatmap: 'getAccountConversationHeatmapData',
|
accountConversationHeatmap: 'getAccountConversationHeatmapData',
|
||||||
uiFlags: 'getOverviewUIFlags',
|
uiFlags: 'getOverviewUIFlags',
|
||||||
}),
|
}),
|
||||||
@@ -113,19 +138,10 @@ export default {
|
|||||||
});
|
});
|
||||||
return metric;
|
return metric;
|
||||||
},
|
},
|
||||||
conversationMetrics() {
|
|
||||||
let metric = {};
|
|
||||||
Object.keys(this.accountConversationMetric).forEach(key => {
|
|
||||||
const metricName = this.$t(
|
|
||||||
`OVERVIEW_REPORTS.ACCOUNT_CONVERSATIONS.${OVERVIEW_METRICS[key]}`
|
|
||||||
);
|
|
||||||
metric[metricName] = this.accountConversationMetric[key];
|
|
||||||
});
|
|
||||||
return metric;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('agents/get');
|
this.$store.dispatch('agents/get');
|
||||||
|
this.$store.dispatch('teams/get');
|
||||||
this.fetchAllData();
|
this.fetchAllData();
|
||||||
|
|
||||||
this.$emitter.on('fetch_overview_reports', () => {
|
this.$emitter.on('fetch_overview_reports', () => {
|
||||||
@@ -133,18 +149,24 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onSelectDateFilter(dayFilter) {
|
||||||
|
this.selectedDayFilter = dayFilter;
|
||||||
|
this.fetchHeatmapData();
|
||||||
|
},
|
||||||
fetchAllData() {
|
fetchAllData() {
|
||||||
this.fetchAccountConversationMetric();
|
this.$store.dispatch('fetchAgentConversationMetric');
|
||||||
this.fetchAgentConversationMetric();
|
this.$store.dispatch('fetchTeamConversationMetric');
|
||||||
this.fetchHeatmapData();
|
this.fetchHeatmapData();
|
||||||
},
|
},
|
||||||
downloadHeatmapData() {
|
downloadHeatmapData() {
|
||||||
let to = endOfDay(new Date());
|
let to = endOfDay(new Date());
|
||||||
|
|
||||||
this.$store.dispatch('downloadAccountConversationHeatmap', {
|
this.$store.dispatch('downloadAccountConversationHeatmap', {
|
||||||
|
daysBefore: this.selectedDayFilter?.id === 1 ? 6 : 29,
|
||||||
to: getUnixTime(to),
|
to: getUnixTime(to),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchHeatmapData() {
|
fetchHeatmapData() {
|
||||||
if (this.uiFlags.isFetchingAccountConversationsHeatmap) {
|
if (this.uiFlags.isFetchingAccountConversationsHeatmap) {
|
||||||
return;
|
return;
|
||||||
@@ -157,12 +179,9 @@ export default {
|
|||||||
// and reconcile it with the rest of the data
|
// and reconcile it with the rest of the data
|
||||||
// this will reduce the load on the server doing number crunching
|
// this will reduce the load on the server doing number crunching
|
||||||
let to = endOfDay(new Date());
|
let to = endOfDay(new Date());
|
||||||
let from = startOfDay(subDays(to, 6));
|
let from = startOfDay(
|
||||||
|
subDays(to, this.selectedDayFilter?.id === 1 ? 6 : 29)
|
||||||
if (this.accountConversationHeatmap.length) {
|
);
|
||||||
to = endOfDay(new Date());
|
|
||||||
from = startOfDay(to);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$store.dispatch('fetchAccountConversationHeatmap', {
|
this.$store.dispatch('fetchAccountConversationHeatmap', {
|
||||||
metric: 'conversations_count',
|
metric: 'conversations_count',
|
||||||
@@ -172,21 +191,6 @@ export default {
|
|||||||
businessHours: false,
|
businessHours: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchAccountConversationMetric() {
|
|
||||||
this.$store.dispatch('fetchAccountConversationMetric', {
|
|
||||||
type: 'account',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
fetchAgentConversationMetric() {
|
|
||||||
this.$store.dispatch('fetchAgentConversationMetric', {
|
|
||||||
type: 'agent',
|
|
||||||
page: this.pageIndex,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onPageNumberChange(pageIndex) {
|
|
||||||
this.pageIndex = pageIndex;
|
|
||||||
this.fetchAgentConversationMetric();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,19 +1,47 @@
|
|||||||
<template>
|
<template>
|
||||||
<woot-reports
|
<div class="column overflow-auto">
|
||||||
key="team-reports"
|
<metric-card :is-live="false" header="All teams overview">
|
||||||
type="team"
|
<woot-summary-reports
|
||||||
getter-key="teams/getTeams"
|
key="team-reports"
|
||||||
action-key="teams/get"
|
class="!p-0"
|
||||||
:download-button-label="$t('TEAM_REPORTS.DOWNLOAD_TEAM_REPORTS')"
|
type="team"
|
||||||
/>
|
getter-key="teams/getTeams"
|
||||||
|
attribute-key="team_id"
|
||||||
|
action-key="summaryReports/fetchTeamSummaryReports"
|
||||||
|
summary-key="summaryReports/getTeamSummaryReports"
|
||||||
|
:download-button-label="$t('TEAM_REPORTS.DOWNLOAD_TEAM_REPORTS')"
|
||||||
|
/>
|
||||||
|
</metric-card>
|
||||||
|
<metric-card
|
||||||
|
:is-live="false"
|
||||||
|
header="Team-wise reports"
|
||||||
|
class="overflow-visible"
|
||||||
|
>
|
||||||
|
<woot-reports
|
||||||
|
key="team-reports"
|
||||||
|
:show-download-button="false"
|
||||||
|
class="!p-0"
|
||||||
|
type="team"
|
||||||
|
getter-key="teams/getTeams"
|
||||||
|
action-key="teams/get"
|
||||||
|
/>
|
||||||
|
</metric-card>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import WootSummaryReports from './components/WootSummaryReports.vue';
|
||||||
|
import MetricCard from './components/overview/MetricCard.vue';
|
||||||
import WootReports from './components/WootReports.vue';
|
import WootReports from './components/WootReports.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
WootReports,
|
WootReports,
|
||||||
|
MetricCard,
|
||||||
|
WootSummaryReports,
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('agents/get');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<div class="agent-table-container">
|
||||||
|
<ve-table
|
||||||
|
max-height="calc(100vh - 21.875rem)"
|
||||||
|
:fixed-header="true"
|
||||||
|
:columns="columns"
|
||||||
|
:table-data="tableData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { VeTable } from 'vue-easytable';
|
||||||
|
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TeamTable',
|
||||||
|
components: {
|
||||||
|
VeTable,
|
||||||
|
},
|
||||||
|
mixins: [rtlMixin],
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
teams: 'teams/getTeams',
|
||||||
|
teamMetrics: 'summaryReports/getTeamSummaryReports',
|
||||||
|
}),
|
||||||
|
|
||||||
|
columns() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'agent',
|
||||||
|
key: 'agent',
|
||||||
|
title: 'Team',
|
||||||
|
fixed: 'left',
|
||||||
|
align: this.isRTLView ? 'right' : 'left',
|
||||||
|
width: 25,
|
||||||
|
renderBodyCell: ({ row }) => (
|
||||||
|
<div class="row-user-block">
|
||||||
|
<div class="user-block">
|
||||||
|
<h6 class="title overflow-hidden whitespace-nowrap text-ellipsis">
|
||||||
|
{row.name}
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'conversationsCount',
|
||||||
|
key: 'conversationsCount',
|
||||||
|
title: 'No. of conversations',
|
||||||
|
align: this.isRTLView ? 'right' : 'left',
|
||||||
|
width: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'resolutionsCount',
|
||||||
|
key: 'resolutionsCount',
|
||||||
|
title: 'No. of resolved conversations',
|
||||||
|
align: this.isRTLView ? 'right' : 'left',
|
||||||
|
width: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'avgFirstResponseTime',
|
||||||
|
key: 'avgFirstResponseTime',
|
||||||
|
title: 'Average first response time',
|
||||||
|
align: this.isRTLView ? 'right' : 'left',
|
||||||
|
width: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'avgResolutionTime',
|
||||||
|
key: 'avgResolutionTime',
|
||||||
|
title: 'Average resolution time',
|
||||||
|
align: this.isRTLView ? 'right' : 'left',
|
||||||
|
width: 20,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('summaryReports/fetchTeamSummaryReports');
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getTeamMetrics(id) {
|
||||||
|
return (
|
||||||
|
this.teamMetrics.find(metrics => metrics.team_id === Number(id)) || {}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.agent-table-container {
|
||||||
|
@apply flex flex-col flex-1;
|
||||||
|
|
||||||
|
.ve-table {
|
||||||
|
&::v-deep {
|
||||||
|
th.ve-table-header-th {
|
||||||
|
font-size: var(--font-size-mini) !important;
|
||||||
|
padding: var(--space-small) var(--space-two) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.ve-table-body-td {
|
||||||
|
padding: var(--space-one) var(--space-two) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::v-deep .ve-pagination {
|
||||||
|
@apply bg-transparent dark:bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::v-deep .ve-pagination-select {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-user-block {
|
||||||
|
@apply items-center flex text-left;
|
||||||
|
|
||||||
|
.user-block {
|
||||||
|
@apply items-start flex flex-col min-w-0 my-0 mx-2;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@apply text-sm m-0 leading-[1.2] text-slate-800 dark:text-slate-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-title {
|
||||||
|
@apply text-xs text-slate-600 dark:text-slate-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-pagination {
|
||||||
|
@apply mt-4 text-right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-loader {
|
||||||
|
@apply items-center flex text-base justify-center p-8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { OVERVIEW_METRICS } from '../../constants';
|
||||||
|
import MetricCard from '../overview/MetricCard.vue';
|
||||||
|
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||||
|
const noneTeam = { team_id: 0, name: 'All teams' };
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MultiselectDropdown,
|
||||||
|
MetricCard,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedTeam: noneTeam,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
teams: 'teams/getTeams',
|
||||||
|
accountConversationMetric: 'getAccountConversationMetric',
|
||||||
|
uiFlags: 'getOverviewUIFlags',
|
||||||
|
}),
|
||||||
|
conversationMetrics() {
|
||||||
|
let metric = {};
|
||||||
|
Object.keys(this.accountConversationMetric).forEach(key => {
|
||||||
|
const metricName = this.$t(
|
||||||
|
`OVERVIEW_REPORTS.ACCOUNT_CONVERSATIONS.${OVERVIEW_METRICS[key]}`
|
||||||
|
);
|
||||||
|
metric[metricName] = this.accountConversationMetric[key];
|
||||||
|
});
|
||||||
|
return metric;
|
||||||
|
},
|
||||||
|
teamsList() {
|
||||||
|
return [noneTeam, ...this.teams];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('fetchLiveConversationMetric');
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSelectTeam(team) {
|
||||||
|
this.$store.dispatch('fetchLiveConversationMetric', {
|
||||||
|
team_id: !team.id ? null : team.id,
|
||||||
|
});
|
||||||
|
this.selectedTeam = team;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex-1 w-full max-w-full md:w-[65%] md:max-w-[65%] conversation-metric"
|
||||||
|
>
|
||||||
|
<metric-card
|
||||||
|
class="!overflow-visible min-h-[150px]"
|
||||||
|
:header="$t('OVERVIEW_REPORTS.ACCOUNT_CONVERSATIONS.HEADER')"
|
||||||
|
:is-loading="uiFlags.isFetchingAccountConversationMetric"
|
||||||
|
loading-message="Loading metrics"
|
||||||
|
>
|
||||||
|
<template #control>
|
||||||
|
<multiselect-dropdown
|
||||||
|
class="!mb-0 !w-1/2"
|
||||||
|
:options="teamsList"
|
||||||
|
:selected-item="selectedTeam"
|
||||||
|
multiselector-title=""
|
||||||
|
multiselector-placeholder="All teams"
|
||||||
|
no-search-result="No teams found"
|
||||||
|
input-placeholder="Search for a team"
|
||||||
|
:is-filter="true"
|
||||||
|
@click="onSelectTeam"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-for="(metric, name, index) in conversationMetrics"
|
||||||
|
:key="index"
|
||||||
|
class="metric-content column"
|
||||||
|
>
|
||||||
|
<h3 class="heading">
|
||||||
|
{{ name }}
|
||||||
|
</h3>
|
||||||
|
<p class="metric">{{ metric }}</p>
|
||||||
|
</div>
|
||||||
|
</metric-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
+2
@@ -110,6 +110,7 @@
|
|||||||
v-model="currentSelectedFilter"
|
v-model="currentSelectedFilter"
|
||||||
track-by="id"
|
track-by="id"
|
||||||
label="name"
|
label="name"
|
||||||
|
open-direction="below"
|
||||||
:placeholder="multiselectLabel"
|
:placeholder="multiselectLabel"
|
||||||
selected-label
|
selected-label
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
@@ -127,6 +128,7 @@
|
|||||||
<multiselect
|
<multiselect
|
||||||
v-model="currentDateRangeSelection"
|
v-model="currentDateRangeSelection"
|
||||||
track-by="name"
|
track-by="name"
|
||||||
|
open-direction="below"
|
||||||
label="name"
|
label="name"
|
||||||
:placeholder="$t('FORMS.MULTISELECT.SELECT_ONE')"
|
:placeholder="$t('FORMS.MULTISELECT.SELECT_ONE')"
|
||||||
selected-label
|
selected-label
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex-1 overflow-auto p-4">
|
<div class="flex-1 overflow-auto p-4">
|
||||||
<woot-button
|
<woot-button
|
||||||
|
v-if="showDownloadButton"
|
||||||
color-scheme="success"
|
color-scheme="success"
|
||||||
class-names="button--fixed-top"
|
class-names="button--fixed-top"
|
||||||
icon="arrow-download"
|
icon="arrow-download"
|
||||||
@@ -64,6 +65,10 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'Download Reports',
|
default: 'Download Reports',
|
||||||
},
|
},
|
||||||
|
showDownloadButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
+180
@@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex-1 overflow-auto p-4">
|
||||||
|
<woot-button
|
||||||
|
color-scheme="success"
|
||||||
|
class-names="button--fixed-top"
|
||||||
|
icon="arrow-download"
|
||||||
|
@click="downloadReports"
|
||||||
|
>
|
||||||
|
{{ downloadButtonLabel }}
|
||||||
|
</woot-button>
|
||||||
|
<report-filter-selector
|
||||||
|
:show-agents-filter="false"
|
||||||
|
:show-group-by-filter="false"
|
||||||
|
@filter-change="onFilterChange"
|
||||||
|
/>
|
||||||
|
<ve-table
|
||||||
|
max-height="calc(100vh - 21.875rem)"
|
||||||
|
:fixed-header="true"
|
||||||
|
:columns="columns"
|
||||||
|
:table-data="tableData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ReportFilterSelector from './FilterSelector.vue';
|
||||||
|
import { formatTime } from '@chatwoot/utils';
|
||||||
|
|
||||||
|
import reportMixin from '../../../../../mixins/reportMixin';
|
||||||
|
import { generateFileName } from '../../../../../helper/downloadHelper';
|
||||||
|
import { VeTable } from 'vue-easytable';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
VeTable,
|
||||||
|
ReportFilterSelector,
|
||||||
|
},
|
||||||
|
mixins: [reportMixin],
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'account',
|
||||||
|
},
|
||||||
|
getterKey: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
actionKey: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
summaryKey: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
downloadButtonLabel: {
|
||||||
|
type: String,
|
||||||
|
default: 'Download Reports',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
selectedFilter: null,
|
||||||
|
businessHours: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
columns() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'agent',
|
||||||
|
key: 'agent',
|
||||||
|
title: this.type,
|
||||||
|
fixed: 'left',
|
||||||
|
align: this.isRTLView ? 'right' : 'left',
|
||||||
|
width: 25,
|
||||||
|
renderBodyCell: ({ row }) => (
|
||||||
|
<div class="row-user-block">
|
||||||
|
<div class="user-block">
|
||||||
|
<h6 class="title overflow-hidden whitespace-nowrap text-ellipsis text-sm capitalize">
|
||||||
|
{row.name}
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'conversationsCount',
|
||||||
|
key: 'conversationsCount',
|
||||||
|
title: 'Assigned',
|
||||||
|
align: this.isRTLView ? 'right' : 'left',
|
||||||
|
width: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'resolutionsCount',
|
||||||
|
key: 'resolutionsCount',
|
||||||
|
title: 'Resolved',
|
||||||
|
align: this.isRTLView ? 'right' : 'left',
|
||||||
|
width: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'avgFirstResponseTime',
|
||||||
|
key: 'avgFirstResponseTime',
|
||||||
|
title: 'Avg. first response time',
|
||||||
|
align: this.isRTLView ? 'right' : 'left',
|
||||||
|
width: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'avgResolutionTime',
|
||||||
|
key: 'avgResolutionTime',
|
||||||
|
title: 'Avg. resolution time',
|
||||||
|
align: this.isRTLView ? 'right' : 'left',
|
||||||
|
width: 20,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
tableData() {
|
||||||
|
return this.filterItemsList.map(team => {
|
||||||
|
const typeMetrics = this.getMetrics(team.id);
|
||||||
|
return {
|
||||||
|
name: team.name,
|
||||||
|
conversationsCount: typeMetrics.conversations_count || '--',
|
||||||
|
avgFirstResponseTime:
|
||||||
|
this.renderContent(typeMetrics.avg_first_response_time) || '--',
|
||||||
|
avgResolutionTime:
|
||||||
|
this.renderContent(typeMetrics.avg_resolution_time) || '--',
|
||||||
|
resolutionsCount: typeMetrics.resolved_conversations_count || '--',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
filterItemsList() {
|
||||||
|
return this.$store.getters[this.getterKey] || [];
|
||||||
|
},
|
||||||
|
typeMetrics() {
|
||||||
|
return this.$store.getters[this.summaryKey] || [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchAllData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
renderContent(value) {
|
||||||
|
return value ? formatTime(value) : '--';
|
||||||
|
},
|
||||||
|
getMetrics(id) {
|
||||||
|
return this.typeMetrics.find(metrics => metrics.id === Number(id)) || {};
|
||||||
|
},
|
||||||
|
fetchAllData() {
|
||||||
|
const { from, to, businessHours } = this;
|
||||||
|
this.$store.dispatch(this.actionKey, {
|
||||||
|
since: from,
|
||||||
|
until: to,
|
||||||
|
businessHours,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
downloadReports() {
|
||||||
|
const { from, to, type, businessHours } = this;
|
||||||
|
const dispatchMethods = {
|
||||||
|
agent: 'downloadAgentReports',
|
||||||
|
label: 'downloadLabelReports',
|
||||||
|
inbox: 'downloadInboxReports',
|
||||||
|
team: 'downloadTeamReports',
|
||||||
|
};
|
||||||
|
if (dispatchMethods[type]) {
|
||||||
|
const fileName = generateFileName({ type, to, businessHours });
|
||||||
|
const params = { from, to, fileName, businessHours };
|
||||||
|
this.$store.dispatch(dispatchMethods[type], params);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFilterChange({ from, to, businessHours }) {
|
||||||
|
this.from = from;
|
||||||
|
this.to = to;
|
||||||
|
this.businessHours = businessHours;
|
||||||
|
this.fetchAllData();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
+19
-24
@@ -12,17 +12,12 @@
|
|||||||
$t('OVERVIEW_REPORTS.AGENT_CONVERSATIONS.LOADING_MESSAGE')
|
$t('OVERVIEW_REPORTS.AGENT_CONVERSATIONS.LOADING_MESSAGE')
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<empty-state
|
<div v-if="agents.length > 0" class="table-pagination">
|
||||||
v-else-if="!isLoading && !agentMetrics.length"
|
|
||||||
:title="$t('OVERVIEW_REPORTS.AGENT_CONVERSATIONS.NO_AGENTS')"
|
|
||||||
/>
|
|
||||||
<div v-if="agentMetrics.length > 0" class="table-pagination">
|
|
||||||
<ve-pagination
|
<ve-pagination
|
||||||
:total="agents.length"
|
:total="agents.length"
|
||||||
:page-index="pageIndex"
|
:page-index="pageIndex"
|
||||||
:page-size="25"
|
:page-size="25"
|
||||||
:page-size-option="[25]"
|
:page-size-option="[25]"
|
||||||
@on-page-number-change="onPageNumberChange"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,14 +26,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { VeTable, VePagination } from 'vue-easytable';
|
import { VeTable, VePagination } from 'vue-easytable';
|
||||||
import Spinner from 'shared/components/Spinner.vue';
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
|
||||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AgentTable',
|
name: 'AgentTable',
|
||||||
components: {
|
components: {
|
||||||
EmptyState,
|
|
||||||
Spinner,
|
Spinner,
|
||||||
VeTable,
|
VeTable,
|
||||||
VePagination,
|
VePagination,
|
||||||
@@ -64,19 +57,17 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
tableData() {
|
tableData() {
|
||||||
return this.agentMetrics
|
return this.agents.map(agent => {
|
||||||
.filter(agentMetric => this.getAgentInformation(agentMetric.id))
|
const agentMetrics = this.getAgentMetrics(agent.id);
|
||||||
.map(agent => {
|
return {
|
||||||
const agentInformation = this.getAgentInformation(agent.id);
|
agent: agent.name,
|
||||||
return {
|
email: agent.email,
|
||||||
agent: agentInformation.name || agentInformation.available_name,
|
thumbnail: agent.thumbnail,
|
||||||
email: agentInformation.email,
|
open: agentMetrics.open || 0,
|
||||||
thumbnail: agentInformation.thumbnail,
|
unattended: agentMetrics.unattended || 0,
|
||||||
open: agent.metric.open || 0,
|
status: agent.availability_status,
|
||||||
unattended: agent.metric.unattended || 0,
|
};
|
||||||
status: agentInformation.availability_status,
|
});
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
columns() {
|
columns() {
|
||||||
return [
|
return [
|
||||||
@@ -128,12 +119,15 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getAgentMetrics(id) {
|
||||||
|
return (
|
||||||
|
this.agentMetrics.find(metrics => metrics.assignee_id === Number(id)) ||
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
},
|
||||||
onPageNumberChange(pageIndex) {
|
onPageNumberChange(pageIndex) {
|
||||||
this.$emit('page-change', pageIndex);
|
this.$emit('page-change', pageIndex);
|
||||||
},
|
},
|
||||||
getAgentInformation(id) {
|
|
||||||
return this.agents?.find(agent => agent.id === Number(id));
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -172,6 +166,7 @@ export default {
|
|||||||
.title {
|
.title {
|
||||||
@apply text-sm m-0 leading-[1.2] text-slate-800 dark:text-slate-100;
|
@apply text-sm m-0 leading-[1.2] text-slate-800 dark:text-slate-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-title {
|
.sub-title {
|
||||||
@apply text-xs text-slate-600 dark:text-slate-200;
|
@apply text-xs text-slate-600 dark:text-slate-200;
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -11,6 +11,7 @@
|
|||||||
{{ header }}
|
{{ header }}
|
||||||
</h5>
|
</h5>
|
||||||
<span
|
<span
|
||||||
|
v-if="isLive"
|
||||||
class="flex flex-row items-center pr-2 pl-2 m-1 rounded-sm text-green-400 dark:text-green-400 text-xs bg-green-100/30 dark:bg-green-100/20"
|
class="flex flex-row items-center pr-2 pl-2 m-1 rounded-sm text-green-400 dark:text-green-400 text-xs bg-green-100/30 dark:bg-green-100/20"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="!isLoading"
|
v-if="!isLoading"
|
||||||
class="card-body max-w-full w-full ml-auto mr-auto justify-between flex"
|
class="card-body max-w-full w-full ml-auto mr-auto justify-between flex"
|
||||||
|
:class="!isLive ? 'px-0' : ''"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
@@ -52,6 +54,10 @@ export default {
|
|||||||
Spinner,
|
Spinner,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
isLive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
header: {
|
header: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
@@ -64,6 +70,10 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
isFilter: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -96,12 +106,18 @@ export default {
|
|||||||
.card-body {
|
.card-body {
|
||||||
.metric-content {
|
.metric-content {
|
||||||
@apply pb-2;
|
@apply pb-2;
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
@apply text-base text-slate-700 dark:text-slate-100;
|
@apply text-base text-slate-700 dark:text-slate-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric {
|
.metric {
|
||||||
@apply text-woot-800 dark:text-woot-300 text-3xl mb-0 mt-1;
|
@apply text-woot-800 dark:text-woot-300 text-3xl mb-0 mt-1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.conversation-metric-loader {
|
||||||
|
@apply items-center flex text-base justify-center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<div class="agent-table-container">
|
||||||
|
<ve-table
|
||||||
|
max-height="calc(100vh - 21.875rem)"
|
||||||
|
:fixed-header="true"
|
||||||
|
:columns="columns"
|
||||||
|
:table-data="tableData"
|
||||||
|
/>
|
||||||
|
<div v-if="isLoading" class="agents-loader">
|
||||||
|
<spinner />
|
||||||
|
<span>{{
|
||||||
|
$t('OVERVIEW_REPORTS.TEAM_CONVERSATIONS.LOADING_MESSAGE')
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="teams.length > 0" class="table-pagination">
|
||||||
|
<ve-pagination
|
||||||
|
:total="teams.length"
|
||||||
|
:page-index="pageIndex"
|
||||||
|
:page-size="25"
|
||||||
|
:page-size-option="[25]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { VeTable, VePagination } from 'vue-easytable';
|
||||||
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
|
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TeamTable',
|
||||||
|
components: {
|
||||||
|
Spinner,
|
||||||
|
VeTable,
|
||||||
|
VePagination,
|
||||||
|
},
|
||||||
|
mixins: [rtlMixin],
|
||||||
|
props: {
|
||||||
|
teams: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
teamMetrics: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
pageIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tableData() {
|
||||||
|
return this.teams.map(team => {
|
||||||
|
const teamMetrics = this.getTeamMetrics(team.id);
|
||||||
|
return {
|
||||||
|
agent: team.name,
|
||||||
|
open: teamMetrics.open || 0,
|
||||||
|
unattended: teamMetrics.unattended || 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
columns() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'agent',
|
||||||
|
key: 'agent',
|
||||||
|
title: this.$t(
|
||||||
|
'OVERVIEW_REPORTS.TEAM_CONVERSATIONS.TABLE_HEADER.AGENT'
|
||||||
|
),
|
||||||
|
fixed: 'left',
|
||||||
|
align: this.isRTLView ? 'right' : 'left',
|
||||||
|
width: 25,
|
||||||
|
renderBodyCell: ({ row }) => (
|
||||||
|
<div class="row-user-block">
|
||||||
|
<div class="user-block">
|
||||||
|
<h6 class="capitalize title overflow-hidden whitespace-nowrap text-ellipsis">
|
||||||
|
{row.agent}
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'open',
|
||||||
|
key: 'open',
|
||||||
|
title: this.$t(
|
||||||
|
'OVERVIEW_REPORTS.TEAM_CONVERSATIONS.TABLE_HEADER.OPEN'
|
||||||
|
),
|
||||||
|
align: this.isRTLView ? 'right' : 'left',
|
||||||
|
width: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'unattended',
|
||||||
|
key: 'unattended',
|
||||||
|
title: this.$t(
|
||||||
|
'OVERVIEW_REPORTS.TEAM_CONVERSATIONS.TABLE_HEADER.UNATTENDED'
|
||||||
|
),
|
||||||
|
align: this.isRTLView ? 'right' : 'left',
|
||||||
|
width: 10,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getTeamMetrics(id) {
|
||||||
|
return (
|
||||||
|
this.teamMetrics.find(metrics => metrics.team_id === Number(id)) || {}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.agent-table-container {
|
||||||
|
@apply flex flex-col flex-1;
|
||||||
|
|
||||||
|
.ve-table {
|
||||||
|
&::v-deep {
|
||||||
|
th.ve-table-header-th {
|
||||||
|
font-size: var(--font-size-mini) !important;
|
||||||
|
padding: var(--space-small) var(--space-two) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.ve-table-body-td {
|
||||||
|
padding: var(--space-one) var(--space-two) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::v-deep .ve-pagination {
|
||||||
|
@apply bg-transparent dark:bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::v-deep .ve-pagination-select {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-user-block {
|
||||||
|
@apply items-center flex text-left;
|
||||||
|
|
||||||
|
.user-block {
|
||||||
|
@apply items-start flex flex-col min-w-0 my-0 mx-2;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@apply text-sm m-0 leading-[1.2] text-slate-800 dark:text-slate-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-title {
|
||||||
|
@apply text-xs text-slate-600 dark:text-slate-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-pagination {
|
||||||
|
@apply mt-4 text-right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-loader {
|
||||||
|
@apply items-center flex text-base justify-center p-8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -42,6 +42,7 @@ import sla from './modules/sla';
|
|||||||
import teamMembers from './modules/teamMembers';
|
import teamMembers from './modules/teamMembers';
|
||||||
import teams from './modules/teams';
|
import teams from './modules/teams';
|
||||||
import userNotificationSettings from './modules/userNotificationSettings';
|
import userNotificationSettings from './modules/userNotificationSettings';
|
||||||
|
import summaryReports from './modules/summaryReports';
|
||||||
import webhooks from './modules/webhooks';
|
import webhooks from './modules/webhooks';
|
||||||
import draftMessages from './modules/draftMessages';
|
import draftMessages from './modules/draftMessages';
|
||||||
import SLAReports from './modules/SLAReports';
|
import SLAReports from './modules/SLAReports';
|
||||||
@@ -90,6 +91,7 @@ export default new Vuex.Store({
|
|||||||
reports,
|
reports,
|
||||||
teamMembers,
|
teamMembers,
|
||||||
teams,
|
teams,
|
||||||
|
summaryReports,
|
||||||
userNotificationSettings,
|
userNotificationSettings,
|
||||||
webhooks,
|
webhooks,
|
||||||
draftMessages,
|
draftMessages,
|
||||||
|
|||||||
@@ -4,10 +4,19 @@ import Report from '../../api/reports';
|
|||||||
import { downloadCsvFile, generateFileName } from '../../helper/downloadHelper';
|
import { downloadCsvFile, generateFileName } from '../../helper/downloadHelper';
|
||||||
import AnalyticsHelper from '../../helper/AnalyticsHelper';
|
import AnalyticsHelper from '../../helper/AnalyticsHelper';
|
||||||
import { REPORTS_EVENTS } from '../../helper/AnalyticsHelper/events';
|
import { REPORTS_EVENTS } from '../../helper/AnalyticsHelper/events';
|
||||||
import {
|
import { clampDataBetweenTimeline } from 'shared/helpers/ReportsDataHelper';
|
||||||
reconcileHeatmapData,
|
import liveReports from '../../api/liveReports';
|
||||||
clampDataBetweenTimeline,
|
|
||||||
} from 'shared/helpers/ReportsDataHelper';
|
const accountSummaryInitialData = {
|
||||||
|
avg_first_response_time: 0,
|
||||||
|
avg_resolution_time: 0,
|
||||||
|
conversations_count: 0,
|
||||||
|
incoming_messages_count: 0,
|
||||||
|
outgoing_messages_count: 0,
|
||||||
|
reply_time: 0,
|
||||||
|
resolutions_count: 0,
|
||||||
|
previous: {},
|
||||||
|
};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
fetchingStatus: false,
|
fetchingStatus: false,
|
||||||
@@ -35,18 +44,7 @@ const state = {
|
|||||||
reply_time: [],
|
reply_time: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
accountSummary: {
|
accountSummary: accountSummaryInitialData,
|
||||||
avg_first_response_time: 0,
|
|
||||||
avg_resolution_time: 0,
|
|
||||||
conversations_count: 0,
|
|
||||||
incoming_messages_count: 0,
|
|
||||||
outgoing_messages_count: 0,
|
|
||||||
reply_time: 0,
|
|
||||||
resolutions_count: 0,
|
|
||||||
bot_resolutions_count: 0,
|
|
||||||
bot_handoffs_count: 0,
|
|
||||||
previous: {},
|
|
||||||
},
|
|
||||||
botSummary: {
|
botSummary: {
|
||||||
bot_resolutions_count: 0,
|
bot_resolutions_count: 0,
|
||||||
bot_handoffs_count: 0,
|
bot_handoffs_count: 0,
|
||||||
@@ -57,10 +55,12 @@ const state = {
|
|||||||
isFetchingAccountConversationMetric: false,
|
isFetchingAccountConversationMetric: false,
|
||||||
isFetchingAccountConversationsHeatmap: false,
|
isFetchingAccountConversationsHeatmap: false,
|
||||||
isFetchingAgentConversationMetric: false,
|
isFetchingAgentConversationMetric: false,
|
||||||
|
isFetchingTeamConversationMetric: false,
|
||||||
},
|
},
|
||||||
accountConversationMetric: {},
|
accountConversationMetric: {},
|
||||||
accountConversationHeatmap: [],
|
accountConversationHeatmap: [],
|
||||||
agentConversationMetric: [],
|
agentConversationMetric: [],
|
||||||
|
teamConversationMetric: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,6 +83,9 @@ const getters = {
|
|||||||
getAgentConversationMetric(_state) {
|
getAgentConversationMetric(_state) {
|
||||||
return _state.overview.agentConversationMetric;
|
return _state.overview.agentConversationMetric;
|
||||||
},
|
},
|
||||||
|
getTeamConversationMetric(_state) {
|
||||||
|
return _state.overview.teamConversationMetric;
|
||||||
|
},
|
||||||
getOverviewUIFlags($state) {
|
getOverviewUIFlags($state) {
|
||||||
return $state.overview.uiFlags;
|
return $state.overview.uiFlags;
|
||||||
},
|
},
|
||||||
@@ -114,16 +117,12 @@ export const actions = {
|
|||||||
let { data } = heatmapData;
|
let { data } = heatmapData;
|
||||||
data = clampDataBetweenTimeline(data, reportObj.from, reportObj.to);
|
data = clampDataBetweenTimeline(data, reportObj.from, reportObj.to);
|
||||||
|
|
||||||
data = reconcileHeatmapData(
|
|
||||||
data,
|
|
||||||
state.overview.accountConversationHeatmap
|
|
||||||
);
|
|
||||||
|
|
||||||
commit(types.default.SET_HEATMAP_DATA, data);
|
commit(types.default.SET_HEATMAP_DATA, data);
|
||||||
commit(types.default.TOGGLE_HEATMAP_LOADING, false);
|
commit(types.default.TOGGLE_HEATMAP_LOADING, false);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchAccountSummary({ commit }, reportObj) {
|
fetchAccountSummary({ commit }, reportObj) {
|
||||||
|
commit(types.default.SET_ACCOUNT_SUMMARY, accountSummaryInitialData);
|
||||||
Report.getSummary(
|
Report.getSummary(
|
||||||
reportObj.from,
|
reportObj.from,
|
||||||
reportObj.to,
|
reportObj.to,
|
||||||
@@ -153,9 +152,10 @@ export const actions = {
|
|||||||
commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, false);
|
commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, false);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchAccountConversationMetric({ commit }, reportObj) {
|
fetchLiveConversationMetric({ commit }, params = {}) {
|
||||||
commit(types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING, true);
|
commit(types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING, true);
|
||||||
Report.getConversationMetric(reportObj.type)
|
liveReports
|
||||||
|
.getConversationMetric(params)
|
||||||
.then(accountConversationMetric => {
|
.then(accountConversationMetric => {
|
||||||
commit(
|
commit(
|
||||||
types.default.SET_ACCOUNT_CONVERSATION_METRIC,
|
types.default.SET_ACCOUNT_CONVERSATION_METRIC,
|
||||||
@@ -167,9 +167,10 @@ export const actions = {
|
|||||||
commit(types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING, false);
|
commit(types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING, false);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchAgentConversationMetric({ commit }, reportObj) {
|
fetchAgentConversationMetric({ commit }) {
|
||||||
commit(types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING, true);
|
commit(types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING, true);
|
||||||
Report.getConversationMetric(reportObj.type, reportObj.page)
|
liveReports
|
||||||
|
.getGroupedConversations()
|
||||||
.then(agentConversationMetric => {
|
.then(agentConversationMetric => {
|
||||||
commit(
|
commit(
|
||||||
types.default.SET_AGENT_CONVERSATION_METRIC,
|
types.default.SET_AGENT_CONVERSATION_METRIC,
|
||||||
@@ -181,6 +182,21 @@ export const actions = {
|
|||||||
commit(types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING, false);
|
commit(types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING, false);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
fetchTeamConversationMetric({ commit }) {
|
||||||
|
commit(types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING, true);
|
||||||
|
liveReports
|
||||||
|
.getGroupedConversations({ groupBy: 'team_id' })
|
||||||
|
.then(agentConversationMetric => {
|
||||||
|
commit(
|
||||||
|
types.default.SET_TEAM_CONVERSATION_METRIC,
|
||||||
|
agentConversationMetric.data
|
||||||
|
);
|
||||||
|
commit(types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING, false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
commit(types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING, false);
|
||||||
|
});
|
||||||
|
},
|
||||||
downloadAgentReports(_, reportObj) {
|
downloadAgentReports(_, reportObj) {
|
||||||
return Report.getAgentReports(reportObj)
|
return Report.getAgentReports(reportObj)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -234,7 +250,7 @@ export const actions = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
downloadAccountConversationHeatmap(_, reportObj) {
|
downloadAccountConversationHeatmap(_, reportObj) {
|
||||||
Report.getConversationTrafficCSV()
|
Report.getConversationTrafficCSV({ daysBefore: reportObj.daysBefore })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
downloadCsvFile(
|
downloadCsvFile(
|
||||||
generateFileName({
|
generateFileName({
|
||||||
@@ -283,9 +299,15 @@ const mutations = {
|
|||||||
[types.default.SET_AGENT_CONVERSATION_METRIC](_state, metricData) {
|
[types.default.SET_AGENT_CONVERSATION_METRIC](_state, metricData) {
|
||||||
_state.overview.agentConversationMetric = metricData;
|
_state.overview.agentConversationMetric = metricData;
|
||||||
},
|
},
|
||||||
|
[types.default.SET_TEAM_CONVERSATION_METRIC](_state, metricData) {
|
||||||
|
_state.overview.teamConversationMetric = metricData;
|
||||||
|
},
|
||||||
[types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING](_state, flag) {
|
[types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING](_state, flag) {
|
||||||
_state.overview.uiFlags.isFetchingAgentConversationMetric = flag;
|
_state.overview.uiFlags.isFetchingAgentConversationMetric = flag;
|
||||||
},
|
},
|
||||||
|
[types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING](_state, flag) {
|
||||||
|
_state.overview.uiFlags.isFetchingTeamConversationMetric = flag;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import SummaryReportsAPI from '../../api/summaryReports';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export const state = {
|
||||||
|
teamSummaryReports: [],
|
||||||
|
agentSummaryReports: [],
|
||||||
|
inboxSummaryReports: [],
|
||||||
|
uiFlags: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
getAgentSummaryReports(_state) {
|
||||||
|
return _state.agentSummaryReports;
|
||||||
|
},
|
||||||
|
getTeamSummaryReports(_state) {
|
||||||
|
return _state.teamSummaryReports;
|
||||||
|
},
|
||||||
|
getInboxSummaryReports(_state) {
|
||||||
|
return _state.inboxSummaryReports;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
async fetchTeamSummaryReports({ commit }, params) {
|
||||||
|
try {
|
||||||
|
const response = await SummaryReportsAPI.getTeamReports(params);
|
||||||
|
commit('setTeamSummaryReport', response.data);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchAgentSummaryReports({ commit }, params) {
|
||||||
|
try {
|
||||||
|
const response = await SummaryReportsAPI.getAgentReports(params);
|
||||||
|
commit('setAgentSummaryReport', response.data);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchInboxSummaryReports({ commit }, params) {
|
||||||
|
try {
|
||||||
|
const response = await SummaryReportsAPI.getInboxReports(params);
|
||||||
|
commit('setInboxSummaryReport', response.data);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
setTeamSummaryReport(_state, data) {
|
||||||
|
Vue.set(_state, 'teamSummaryReports', data);
|
||||||
|
},
|
||||||
|
setAgentSummaryReport(_state, data) {
|
||||||
|
Vue.set(_state, 'agentSummaryReports', data);
|
||||||
|
},
|
||||||
|
setInboxSummaryReport(_state, data) {
|
||||||
|
Vue.set(_state, 'inboxSummaryReports', data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations,
|
||||||
|
};
|
||||||
@@ -182,8 +182,11 @@ export default {
|
|||||||
TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING:
|
TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING:
|
||||||
'TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING',
|
'TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING',
|
||||||
SET_AGENT_CONVERSATION_METRIC: 'SET_AGENT_CONVERSATION_METRIC',
|
SET_AGENT_CONVERSATION_METRIC: 'SET_AGENT_CONVERSATION_METRIC',
|
||||||
|
SET_TEAM_CONVERSATION_METRIC: 'SET_TEAM_CONVERSATION_METRIC',
|
||||||
TOGGLE_AGENT_CONVERSATION_METRIC_LOADING:
|
TOGGLE_AGENT_CONVERSATION_METRIC_LOADING:
|
||||||
'TOGGLE_AGENT_CONVERSATION_METRIC_LOADING',
|
'TOGGLE_AGENT_CONVERSATION_METRIC_LOADING',
|
||||||
|
TOGGLE_TEAM_CONVERSATION_METRIC_LOADING:
|
||||||
|
'TOGGLE_TEAM_CONVERSATION_METRIC_LOADING',
|
||||||
|
|
||||||
// Conversation Metadata
|
// Conversation Metadata
|
||||||
SET_CONVERSATION_METADATA: 'SET_CONVERSATION_METADATA',
|
SET_CONVERSATION_METADATA: 'SET_CONVERSATION_METADATA',
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob
|
|||||||
rescue *ExceptionList::IMAP_EXCEPTIONS => e
|
rescue *ExceptionList::IMAP_EXCEPTIONS => e
|
||||||
Rails.logger.error "Authorization error for email channel - #{channel.inbox.id} : #{e.message}"
|
Rails.logger.error "Authorization error for email channel - #{channel.inbox.id} : #{e.message}"
|
||||||
rescue EOFError, OpenSSL::SSL::SSLError, Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, Net::IMAP::InvalidResponseError => e
|
rescue EOFError, OpenSSL::SSL::SSLError, Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, Net::IMAP::InvalidResponseError => e
|
||||||
Rails.logger.error "Error for email channel - #{channel.inbox.id} : #{e.message}"
|
Rails.logger.error "Net::IMAP Error for email channel - #{channel.inbox.id} : #{e.message}"
|
||||||
rescue LockAcquisitionError
|
rescue LockAcquisitionError
|
||||||
Rails.logger.error "Lock failed for #{channel.inbox.id}"
|
Rails.logger.error "Lock failed for #{channel.inbox.id}"
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
@@ -39,8 +39,8 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob
|
|||||||
process_mail(inbound_mail, channel)
|
process_mail(inbound_mail, channel)
|
||||||
end
|
end
|
||||||
rescue OAuth2::Error => e
|
rescue OAuth2::Error => e
|
||||||
Rails.logger.error "Error for email channel - #{channel.inbox.id} : #{e.message}"
|
Rails.logger.error "OAuth2::Error for email channel - #{channel.inbox.id} : #{e.message}"
|
||||||
channel.authorization_error!
|
# channel.authorization_error!
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_mail(inbound_mail, channel)
|
def process_mail(inbound_mail, channel)
|
||||||
|
|||||||
@@ -11,5 +11,5 @@
|
|||||||
%>
|
%>
|
||||||
<%= CSVSafe.generate_line headers -%>
|
<%= CSVSafe.generate_line headers -%>
|
||||||
<% @report_data.each do |row| %>
|
<% @report_data.each do |row| %>
|
||||||
<%= CSVSafe.generate_line row -%>
|
<%= CSV.generate_line row -%>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<%= CSV.generate_line [I18n.t('reports.conversation_traffic_csv.timezone'), @timezone] %>
|
<%= CSV.generate_line [I18n.t('reports.conversation_traffic_csv.timezone'), @timezone] %>
|
||||||
|
|
||||||
<% @report_data.each do |row| %>
|
<% @report_data.each do |row| %>
|
||||||
<%= CSVSafe.generate_line row -%>
|
<%= CSV.generate_line row -%>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -10,5 +10,5 @@
|
|||||||
%>
|
%>
|
||||||
<%= CSVSafe.generate_line headers -%>
|
<%= CSVSafe.generate_line headers -%>
|
||||||
<% @report_data.each do |row| %>
|
<% @report_data.each do |row| %>
|
||||||
<%= CSVSafe.generate_line row -%>
|
<%= CSV.generate_line row -%>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -9,5 +9,5 @@
|
|||||||
%>
|
%>
|
||||||
<%= CSVSafe.generate_line headers -%>
|
<%= CSVSafe.generate_line headers -%>
|
||||||
<% @report_data.each do |row| %>
|
<% @report_data.each do |row| %>
|
||||||
<%= CSVSafe.generate_line row -%>
|
<%= CSV.generate_line row -%>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -11,5 +11,5 @@
|
|||||||
%>
|
%>
|
||||||
<%= CSVSafe.generate_line headers -%>
|
<%= CSVSafe.generate_line headers -%>
|
||||||
<% @report_data.each do |row| %>
|
<% @report_data.each do |row| %>
|
||||||
<%= CSVSafe.generate_line row -%>
|
<%= CSV.generate_line row -%>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -317,10 +317,17 @@ Rails.application.routes.draw do
|
|||||||
namespace :v2 do
|
namespace :v2 do
|
||||||
resources :accounts, only: [:create] do
|
resources :accounts, only: [:create] do
|
||||||
scope module: :accounts do
|
scope module: :accounts do
|
||||||
|
resources :live_reports, only: [] do
|
||||||
|
collection do
|
||||||
|
get :conversation_metrics
|
||||||
|
get :grouped_conversation_metrics
|
||||||
|
end
|
||||||
|
end
|
||||||
resources :summary_reports, only: [] do
|
resources :summary_reports, only: [] do
|
||||||
collection do
|
collection do
|
||||||
get :agent
|
get :agent
|
||||||
get :team
|
get :team
|
||||||
|
get :inbox
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
resources :reports, only: [:index] do
|
resources :reports, only: [:index] do
|
||||||
|
|||||||
Reference in New Issue
Block a user