mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-29 18:22:53 +00:00
feat(v4): Update team, agent summary builder to include resolution metrics (#10607)
Following https://github.com/chatwoot/chatwoot/pull/10604, this PR introduces similar reporting features for Agents and Teams. Updates in this PR: - Added additional methods to the base class to avoid repetition. - Improve reporting for Teams and Agents to include resolution count.
This commit is contained in:
@@ -2,52 +2,38 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
pattr_initialize [:account!, :params!]
|
||||
|
||||
def build
|
||||
set_grouped_conversations_count
|
||||
set_grouped_avg_reply_time
|
||||
set_grouped_avg_first_response_time
|
||||
set_grouped_avg_resolution_time
|
||||
load_data
|
||||
prepare_report
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_grouped_conversations_count
|
||||
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('assignee_id').count
|
||||
attr_reader :conversations_count, :resolved_count,
|
||||
:avg_resolution_time, :avg_first_response_time, :avg_reply_time
|
||||
|
||||
def fetch_conversations_count
|
||||
account.conversations.where(created_at: range).group('assignee_id').count
|
||||
end
|
||||
|
||||
def set_grouped_avg_resolution_time
|
||||
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
|
||||
def prepare_report
|
||||
account.account_users.map do |account_user|
|
||||
build_agent_stats(account_user)
|
||||
end
|
||||
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'))
|
||||
def build_agent_stats(account_user)
|
||||
user_id = account_user.user_id
|
||||
{
|
||||
id: user_id,
|
||||
conversations_count: conversations_count[user_id] || 0,
|
||||
resolved_conversations_count: resolved_count[user_id] || 0,
|
||||
avg_resolution_time: avg_resolution_time[user_id],
|
||||
avg_first_response_time: avg_first_response_time[user_id],
|
||||
avg_reply_time: avg_reply_time[user_id]
|
||||
}
|
||||
end
|
||||
|
||||
def group_by_key
|
||||
:user_id
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@reporting_events ||= Current.account.reporting_events.where(created_at: range)
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
account.account_users.each_with_object([]) do |account_user, arr|
|
||||
arr << {
|
||||
id: account_user.user_id,
|
||||
conversations_count: @grouped_conversations_count[account_user.user_id],
|
||||
avg_resolution_time: @grouped_avg_resolution_time[account_user.user_id],
|
||||
avg_first_response_time: @grouped_avg_first_response_time[account_user.user_id],
|
||||
avg_reply_time: @grouped_avg_reply_time[account_user.user_id]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def average_value_key
|
||||
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,17 +1,50 @@
|
||||
class V2::Reports::BaseSummaryBuilder
|
||||
include DateRangeHelper
|
||||
|
||||
def build
|
||||
load_data
|
||||
prepare_report
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_data
|
||||
@conversations_count = fetch_conversations_count
|
||||
@resolved_count = fetch_resolved_count
|
||||
@avg_resolution_time = fetch_average_time('conversation_resolved')
|
||||
@avg_first_response_time = fetch_average_time('first_response')
|
||||
@avg_reply_time = fetch_average_time('reply_time')
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@reporting_events ||= account.reporting_events.where(created_at: range)
|
||||
end
|
||||
|
||||
def fetch_conversations_count
|
||||
# Override this method
|
||||
end
|
||||
|
||||
def fetch_average_time(event_name)
|
||||
get_grouped_average(reporting_events.where(name: event_name))
|
||||
end
|
||||
|
||||
def fetch_resolved_count
|
||||
reporting_events.where(name: 'conversation_resolved').group(group_by_key).count
|
||||
end
|
||||
|
||||
def group_by_key
|
||||
# Override this method
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
# Override this method
|
||||
end
|
||||
|
||||
def get_grouped_average(events)
|
||||
events.group(group_by_key).average(average_value_key)
|
||||
end
|
||||
|
||||
def average_value_key
|
||||
params[:business_hours].present? ? :value_in_business_hours : :value
|
||||
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
|
||||
end
|
||||
end
|
||||
|
||||
@@ -23,18 +23,6 @@ class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
account.conversations.where(created_at: range).group(group_by_key).count
|
||||
end
|
||||
|
||||
def fetch_resolved_count
|
||||
reporting_events.where(name: 'conversation_resolved').group(group_by_key).count
|
||||
end
|
||||
|
||||
def fetch_average_time(event_name)
|
||||
get_grouped_average(reporting_events.where(name: event_name))
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@reporting_events ||= account.reporting_events.where(created_at: range)
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
account.inboxes.map do |inbox|
|
||||
build_inbox_stats(inbox)
|
||||
|
||||
@@ -1,49 +1,37 @@
|
||||
class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
pattr_initialize [:account!, :params!]
|
||||
|
||||
def build
|
||||
set_grouped_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('team_id').count
|
||||
end
|
||||
attr_reader :conversations_count, :resolved_count,
|
||||
:avg_resolution_time, :avg_first_response_time, :avg_reply_time
|
||||
|
||||
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'))
|
||||
def fetch_conversations_count
|
||||
account.conversations.where(created_at: range).group(:team_id).count
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation)
|
||||
@reporting_events ||= account.reporting_events.where(created_at: range).joins(:conversation)
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
account.teams.map do |team|
|
||||
build_team_stats(team)
|
||||
end
|
||||
end
|
||||
|
||||
def build_team_stats(team)
|
||||
{
|
||||
id: team.id,
|
||||
conversations_count: conversations_count[team.id] || 0,
|
||||
resolved_conversations_count: resolved_count[team.id] || 0,
|
||||
avg_resolution_time: avg_resolution_time[team.id],
|
||||
avg_first_response_time: avg_first_response_time[team.id],
|
||||
avg_reply_time: avg_reply_time[team.id]
|
||||
}
|
||||
end
|
||||
|
||||
def group_by_key
|
||||
'conversations.team_id'
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
account.teams.each_with_object([]) do |team, arr|
|
||||
arr << {
|
||||
id: team.id,
|
||||
conversations_count: @grouped_conversations_count[team.id],
|
||||
avg_resolution_time: @grouped_avg_resolution_time[team.id],
|
||||
avg_first_response_time: @grouped_avg_first_response_time[team.id],
|
||||
avg_reply_time: @grouped_avg_reply_time[team.id]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
143
spec/builders/v2/reports/agent_summary_builder_spec.rb
Normal file
143
spec/builders/v2/reports/agent_summary_builder_spec.rb
Normal file
@@ -0,0 +1,143 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe V2::Reports::AgentSummaryBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let(:user1) { create(:user, account: account, role: :agent) }
|
||||
let(:user2) { create(:user, account: account, role: :agent) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
business_hours: business_hours,
|
||||
since: 1.week.ago.beginning_of_day,
|
||||
until: Time.current.end_of_day
|
||||
}
|
||||
end
|
||||
let(:builder) { described_class.new(account: account, params: params) }
|
||||
|
||||
describe '#build' do
|
||||
context 'when there is team data' do
|
||||
before do
|
||||
c1 = create(:conversation, account: account, assignee: user1, created_at: Time.current)
|
||||
c2 = create(:conversation, account: account, assignee: user2, created_at: Time.current)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c2,
|
||||
user: user2,
|
||||
name: 'conversation_resolved',
|
||||
value: 50,
|
||||
value_in_business_hours: 40,
|
||||
created_at: Time.current
|
||||
)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c1,
|
||||
user: user1,
|
||||
name: 'first_response',
|
||||
value: 20,
|
||||
value_in_business_hours: 10,
|
||||
created_at: Time.current
|
||||
)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c1,
|
||||
user: user1,
|
||||
name: 'reply_time',
|
||||
value: 30,
|
||||
value_in_business_hours: 15,
|
||||
created_at: Time.current
|
||||
)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c1,
|
||||
user: user1,
|
||||
name: 'reply_time',
|
||||
value: 40,
|
||||
value_in_business_hours: 25,
|
||||
created_at: Time.current
|
||||
)
|
||||
end
|
||||
|
||||
context 'when business hours is disabled' do
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'returns the correct team stats' do
|
||||
report = builder.build
|
||||
|
||||
expect(report).to eq(
|
||||
[
|
||||
{
|
||||
id: user1.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 0,
|
||||
avg_resolution_time: nil,
|
||||
avg_first_response_time: 20.0,
|
||||
avg_reply_time: 35.0
|
||||
},
|
||||
{
|
||||
id: user2.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 1,
|
||||
avg_resolution_time: 50.0,
|
||||
avg_first_response_time: nil,
|
||||
avg_reply_time: nil
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when business hours is enabled' do
|
||||
let(:business_hours) { true }
|
||||
|
||||
it 'uses business hours values' do
|
||||
report = builder.build
|
||||
|
||||
expect(report).to eq(
|
||||
[
|
||||
{
|
||||
id: user1.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 0,
|
||||
avg_resolution_time: nil,
|
||||
avg_first_response_time: 10.0,
|
||||
avg_reply_time: 20.0
|
||||
},
|
||||
{
|
||||
id: user2.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 1,
|
||||
avg_resolution_time: 40.0,
|
||||
avg_first_response_time: nil,
|
||||
avg_reply_time: nil
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no team data' do
|
||||
let!(:new_user) { create(:user, account: account, role: :agent) }
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'returns zero values' do
|
||||
report = builder.build
|
||||
|
||||
expect(report).to include(
|
||||
{
|
||||
id: new_user.id,
|
||||
conversations_count: 0,
|
||||
resolved_conversations_count: 0,
|
||||
avg_resolution_time: nil,
|
||||
avg_first_response_time: nil,
|
||||
avg_reply_time: nil
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
138
spec/builders/v2/reports/team_summary_builder_spec.rb
Normal file
138
spec/builders/v2/reports/team_summary_builder_spec.rb
Normal file
@@ -0,0 +1,138 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe V2::Reports::TeamSummaryBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let(:team1) { create(:team, account: account, name: 'team-1') }
|
||||
let(:team2) { create(:team, account: account, name: 'team-2') }
|
||||
let(:params) do
|
||||
{
|
||||
business_hours: business_hours,
|
||||
since: 1.week.ago.beginning_of_day,
|
||||
until: Time.current.end_of_day
|
||||
}
|
||||
end
|
||||
let(:builder) { described_class.new(account: account, params: params) }
|
||||
|
||||
describe '#build' do
|
||||
context 'when there is team data' do
|
||||
before do
|
||||
c1 = create(:conversation, account: account, team: team1, created_at: Time.current)
|
||||
c2 = create(:conversation, account: account, team: team2, created_at: Time.current)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c2,
|
||||
name: 'conversation_resolved',
|
||||
value: 50,
|
||||
value_in_business_hours: 40,
|
||||
created_at: Time.current
|
||||
)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c1,
|
||||
name: 'first_response',
|
||||
value: 20,
|
||||
value_in_business_hours: 10,
|
||||
created_at: Time.current
|
||||
)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c1,
|
||||
name: 'reply_time',
|
||||
value: 30,
|
||||
value_in_business_hours: 15,
|
||||
created_at: Time.current
|
||||
)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c1,
|
||||
name: 'reply_time',
|
||||
value: 40,
|
||||
value_in_business_hours: 25,
|
||||
created_at: Time.current
|
||||
)
|
||||
end
|
||||
|
||||
context 'when business hours is disabled' do
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'returns the correct team stats' do
|
||||
report = builder.build
|
||||
|
||||
expect(report).to eq(
|
||||
[
|
||||
{
|
||||
id: team1.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 0,
|
||||
avg_resolution_time: nil,
|
||||
avg_first_response_time: 20.0,
|
||||
avg_reply_time: 35.0
|
||||
},
|
||||
{
|
||||
id: team2.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 1,
|
||||
avg_resolution_time: 50.0,
|
||||
avg_first_response_time: nil,
|
||||
avg_reply_time: nil
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when business hours is enabled' do
|
||||
let(:business_hours) { true }
|
||||
|
||||
it 'uses business hours values' do
|
||||
report = builder.build
|
||||
|
||||
expect(report).to eq(
|
||||
[
|
||||
{
|
||||
id: team1.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 0,
|
||||
avg_resolution_time: nil,
|
||||
avg_first_response_time: 10.0,
|
||||
avg_reply_time: 20.0
|
||||
},
|
||||
{
|
||||
id: team2.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 1,
|
||||
avg_resolution_time: 40.0,
|
||||
avg_first_response_time: nil,
|
||||
avg_reply_time: nil
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no team data' do
|
||||
let!(:new_team) { create(:team, account: account) }
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'returns zero values' do
|
||||
report = builder.build
|
||||
|
||||
expect(report).to include(
|
||||
{
|
||||
id: new_team.id,
|
||||
conversations_count: 0,
|
||||
resolved_conversations_count: 0,
|
||||
avg_resolution_time: nil,
|
||||
avg_first_response_time: nil,
|
||||
avg_reply_time: nil
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user