mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +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!] |   pattr_initialize [:account!, :params!] | ||||||
|  |  | ||||||
|   def build |   def build | ||||||
|     set_grouped_conversations_count |     load_data | ||||||
|     set_grouped_avg_reply_time |  | ||||||
|     set_grouped_avg_first_response_time |  | ||||||
|     set_grouped_avg_resolution_time |  | ||||||
|     prepare_report |     prepare_report | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   private |   private | ||||||
|  |  | ||||||
|   def set_grouped_conversations_count |   attr_reader :conversations_count, :resolved_count, | ||||||
|     @grouped_conversations_count = Current.account.conversations.where(created_at: range).group('assignee_id').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 |   end | ||||||
|  |  | ||||||
|   def set_grouped_avg_resolution_time |   def prepare_report | ||||||
|     @grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved')) |     account.account_users.map do |account_user| | ||||||
|  |       build_agent_stats(account_user) | ||||||
|  |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def set_grouped_avg_first_response_time |   def build_agent_stats(account_user) | ||||||
|     @grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response')) |     user_id = account_user.user_id | ||||||
|   end |     { | ||||||
|  |       id: user_id, | ||||||
|   def set_grouped_avg_reply_time |       conversations_count: conversations_count[user_id] || 0, | ||||||
|     @grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time')) |       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 |   end | ||||||
|  |  | ||||||
|   def group_by_key |   def group_by_key | ||||||
|     :user_id |     :user_id | ||||||
|   end |   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 | end | ||||||
|   | |||||||
| @@ -1,17 +1,50 @@ | |||||||
| class V2::Reports::BaseSummaryBuilder | class V2::Reports::BaseSummaryBuilder | ||||||
|   include DateRangeHelper |   include DateRangeHelper | ||||||
|  |  | ||||||
|  |   def build | ||||||
|  |     load_data | ||||||
|  |     prepare_report | ||||||
|  |   end | ||||||
|  |  | ||||||
|   private |   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 |   def group_by_key | ||||||
|     # Override this method |     # Override this method | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def prepare_report | ||||||
|  |     # Override this method | ||||||
|  |   end | ||||||
|  |  | ||||||
|   def get_grouped_average(events) |   def get_grouped_average(events) | ||||||
|     events.group(group_by_key).average(average_value_key) |     events.group(group_by_key).average(average_value_key) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def average_value_key |   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 | ||||||
| end | end | ||||||
|   | |||||||
| @@ -23,18 +23,6 @@ class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder | |||||||
|     account.conversations.where(created_at: range).group(group_by_key).count |     account.conversations.where(created_at: range).group(group_by_key).count | ||||||
|   end |   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 |   def prepare_report | ||||||
|     account.inboxes.map do |inbox| |     account.inboxes.map do |inbox| | ||||||
|       build_inbox_stats(inbox) |       build_inbox_stats(inbox) | ||||||
|   | |||||||
| @@ -1,49 +1,37 @@ | |||||||
| class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder | class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder | ||||||
|   pattr_initialize [:account!, :params!] |   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 |   private | ||||||
|  |  | ||||||
|   def set_grouped_conversations_count |   attr_reader :conversations_count, :resolved_count, | ||||||
|     @grouped_conversations_count = Current.account.conversations.where(created_at: range).group('team_id').count |               :avg_resolution_time, :avg_first_response_time, :avg_reply_time | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def set_grouped_avg_resolution_time |   def fetch_conversations_count | ||||||
|     @grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved')) |     account.conversations.where(created_at: range).group(:team_id).count | ||||||
|   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 |   end | ||||||
|  |  | ||||||
|   def reporting_events |   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 |   end | ||||||
|  |  | ||||||
|   def group_by_key |   def group_by_key | ||||||
|     'conversations.team_id' |     'conversations.team_id' | ||||||
|   end |   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 | 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
	 Pranav
					Pranav