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:
Pranav
2024-12-20 05:46:56 -08:00
committed by GitHub
parent 4fd9bddb9d
commit c52282307a
6 changed files with 357 additions and 81 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View 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

View 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