mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-30 18:47:51 +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