mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	feat: Add new APIs for live reports with team filter (#10994)
This PR is part of the larger #10849 implementation and introduces a new Live Reports API to provide real-time conversation metrics. The /live_reports/conversation_metrics endpoint returns account-level or team-level conversation statistics, including open, pending, unattended, and unassigned conversation counts. The /live_reports/grouped_conversation_metrics endpoint accepts a group parameter, either team_id or assignee_id, and returns open and unattended conversation counts based on the specified grouping.
This commit is contained in:
		
							
								
								
									
										64
									
								
								app/controllers/api/v2/accounts/live_reports_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								app/controllers/api/v2/accounts/live_reports_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| 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] | ||||
|  | ||||
|   before_action :check_authorization | ||||
|  | ||||
|   def conversation_metrics | ||||
|     render json: { | ||||
|       open: @conversations.open.count, | ||||
|       unattended: @conversations.open.unattended.count, | ||||
|       unassigned: @conversations.open.unassigned.count, | ||||
|       pending: @conversations.pending.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 check_authorization | ||||
|     authorize :report, :view? | ||||
|   end | ||||
|  | ||||
|   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 | ||||
| @@ -337,6 +337,12 @@ Rails.application.routes.draw do | ||||
|               get :bot_metrics | ||||
|             end | ||||
|           end | ||||
|           resources :live_reports, only: [] do | ||||
|             collection do | ||||
|               get :conversation_metrics | ||||
|               get :grouped_conversation_metrics | ||||
|             end | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   | ||||
							
								
								
									
										171
									
								
								spec/controllers/api/v2/accounts/live_reports_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								spec/controllers/api/v2/accounts/live_reports_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,171 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| RSpec.describe 'Api::V2::Accounts::LiveReports', type: :request do | ||||
|   let(:account) { create(:account) } | ||||
|   let(:admin) { create(:user, account: account, role: :administrator) } | ||||
|   let(:agent) { create(:user, account: account, role: :agent) } | ||||
|   let!(:team) { create(:team, account: account) } | ||||
|   let(:team_member) { create(:team_member, team: team, user: admin) } | ||||
|  | ||||
|   describe 'GET /api/v2/accounts/{account.id}/live_reports/conversation_metrics' do | ||||
|     context 'when unauthenticated' do | ||||
|       it 'returns unauthorized' do | ||||
|         get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics" | ||||
|         expect(response).to have_http_status(:unauthorized) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when authenticated but not authorized' do | ||||
|       it 'returns forbidden' do | ||||
|         get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics", | ||||
|             headers: agent.create_new_auth_token, | ||||
|             as: :json | ||||
|         expect(response).to have_http_status(:unauthorized) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when authenticated and authorized' do | ||||
|       before do | ||||
|         create(:conversation, :with_assignee, account: account, status: :open) | ||||
|         create(:conversation, account: account, status: :open) | ||||
|         create(:conversation, :with_assignee, account: account, status: :pending) | ||||
|         create(:conversation, :with_assignee, account: account, status: :open) do |conversation| | ||||
|           create(:message, account: account, conversation: conversation, message_type: :outgoing) | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       it 'returns conversation metrics' do | ||||
|         get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics", | ||||
|             headers: admin.create_new_auth_token, | ||||
|             as: :json | ||||
|  | ||||
|         expect(response).to have_http_status(:success) | ||||
|  | ||||
|         response_data = response.parsed_body | ||||
|         expect(response_data['open']).to eq(3) | ||||
|         expect(response_data['unattended']).to eq(2) | ||||
|         expect(response_data['unassigned']).to eq(1) | ||||
|         expect(response_data['pending']).to eq(1) | ||||
|       end | ||||
|  | ||||
|       context 'with team_id parameter' do | ||||
|         before do | ||||
|           create(:conversation, account: account, status: :open, team_id: team.id) | ||||
|           create(:conversation, account: account, status: :open) | ||||
|         end | ||||
|  | ||||
|         it 'returns metrics filtered by team' do | ||||
|           get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics", | ||||
|               params: { team_id: team.id }, | ||||
|               headers: admin.create_new_auth_token, | ||||
|               as: :json | ||||
|  | ||||
|           expect(response).to have_http_status(:success) | ||||
|  | ||||
|           response_data = response.parsed_body | ||||
|           expect(response_data['open']).to eq(1) | ||||
|           expect(response_data['unattended']).to eq(1) | ||||
|           expect(response_data['unassigned']).to eq(1) | ||||
|           expect(response_data['pending']).to eq(0) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'GET /api/v2/accounts/{account.id}/live_reports/grouped_conversation_metrics' do | ||||
|     context 'when unauthenticated' do | ||||
|       it 'returns unauthorized' do | ||||
|         get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics", | ||||
|             params: { group_by: 'team_id' } | ||||
|         expect(response).to have_http_status(:unauthorized) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when authenticated but not authorized' do | ||||
|       it 'returns forbidden' do | ||||
|         get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics", | ||||
|             params: { group_by: 'team_id' }, | ||||
|             headers: agent.create_new_auth_token, | ||||
|             as: :json | ||||
|         expect(response).to have_http_status(:unauthorized) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'with invalid group_by parameter' do | ||||
|       it 'returns unprocessable_entity error' do | ||||
|         get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics", | ||||
|             params: { group_by: 'invalid_param' }, | ||||
|             headers: admin.create_new_auth_token, | ||||
|             as: :json | ||||
|  | ||||
|         expect(response).to have_http_status(:unprocessable_entity) | ||||
|         expect(response.parsed_body['error']).to eq('invalid group_by') | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when grouped by team_id' do | ||||
|       let(:assignee1) { create(:user, account: account) } | ||||
|  | ||||
|       before do | ||||
|         create(:conversation, account: account, status: :open, team_id: team.id) | ||||
|         create(:conversation, account: account, status: :open, team_id: team.id) | ||||
|         create(:conversation, account: account, status: :open, team_id: team.id) do |conversation| | ||||
|           create(:message, account: account, conversation: conversation, message_type: :outgoing) | ||||
|         end | ||||
|  | ||||
|         create(:conversation, account: account, status: :open, assignee_id: assignee1.id) | ||||
|         create(:conversation, account: account, status: :open) do |conversation| | ||||
|           create(:message, account: account, conversation: conversation, message_type: :outgoing) | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       it 'returns metrics grouped by team' do | ||||
|         get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics", | ||||
|             params: { group_by: 'team_id' }, | ||||
|             headers: admin.create_new_auth_token, | ||||
|             as: :json | ||||
|  | ||||
|         expect(response).to have_http_status(:success) | ||||
|  | ||||
|         response_data = response.parsed_body | ||||
|         expect(response_data.size).to eq(2) | ||||
|         expect(response_data).to include( | ||||
|           { 'team_id' => nil, 'open' => 2, 'unattended' => 1, 'unassigned' => 1 } | ||||
|         ) | ||||
|         expect(response_data).to include( | ||||
|           { 'team_id' => team.id, 'open' => 3, 'unattended' => 2, 'unassigned' => 3 } | ||||
|         ) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when filtering by assignee_id' do | ||||
|       let(:assignee1) { create(:user, account: account) } | ||||
|  | ||||
|       before do | ||||
|         create(:conversation, assignee_id: agent.id, account: account, status: :open) | ||||
|         create(:conversation, account: account, status: :open) | ||||
|         create(:conversation, assignee_id: agent.id, account: account, status: :open) do |conversation| | ||||
|           create(:message, account: account, conversation: conversation, message_type: :outgoing) | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       it 'returns metrics grouped by assignee' do | ||||
|         get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics", | ||||
|             params: { group_by: 'assignee_id' }, | ||||
|             headers: admin.create_new_auth_token, | ||||
|             as: :json | ||||
|  | ||||
|         expect(response).to have_http_status(:success) | ||||
|  | ||||
|         response_data = response.parsed_body | ||||
|         expect(response_data.size).to eq 2 | ||||
|         expect(response_data).to include( | ||||
|           { 'assignee_id' => agent.id, 'open' => 2, 'unassigned' => 0, 'unattended' => 1 } | ||||
|         ) | ||||
|         expect(response_data).to include( | ||||
|           { 'assignee_id' => nil, 'open' => 1, 'unassigned' => 1, 'unattended' => 1 } | ||||
|         ) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @@ -1,6 +1,6 @@ | ||||
| FactoryBot.define do | ||||
|   factory :team do | ||||
|     name { 'MyString' } | ||||
|     sequence(:name) { |n| "Team #{n}" } | ||||
|     description { 'MyText' } | ||||
|     allow_auto_assign { true } | ||||
|     account | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Pranav
					Pranav