feat: Add inbox-label matrix report endpoint (#13394)
This PR added new API endpoint GET /api/v2/accounts/:account_id/reports/inbox_label_matrix that returns conversation counts grouped by inbox and label in a matrix format. Supports optional filtering by date range, inbox_ids, and label_ids. --------- Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
65
app/builders/v2/reports/inbox_label_matrix_builder.rb
Normal file
65
app/builders/v2/reports/inbox_label_matrix_builder.rb
Normal file
@@ -0,0 +1,65 @@
|
||||
class V2::Reports::InboxLabelMatrixBuilder
|
||||
include DateRangeHelper
|
||||
|
||||
attr_reader :account, :params
|
||||
|
||||
def initialize(account:, params:)
|
||||
@account = account
|
||||
@params = params
|
||||
end
|
||||
|
||||
def build
|
||||
{
|
||||
inboxes: filtered_inboxes.map { |inbox| { id: inbox.id, name: inbox.name } },
|
||||
labels: filtered_labels.map { |label| { id: label.id, title: label.title } },
|
||||
matrix: build_matrix
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filtered_inboxes
|
||||
@filtered_inboxes ||= begin
|
||||
inboxes = account.inboxes
|
||||
inboxes = inboxes.where(id: params[:inbox_ids]) if params[:inbox_ids].present?
|
||||
inboxes.order(:name).to_a
|
||||
end
|
||||
end
|
||||
|
||||
def filtered_labels
|
||||
@filtered_labels ||= begin
|
||||
labels = account.labels
|
||||
labels = labels.where(id: params[:label_ids]) if params[:label_ids].present?
|
||||
labels.order(:title).to_a
|
||||
end
|
||||
end
|
||||
|
||||
def conversation_filter
|
||||
filter = { account_id: account.id }
|
||||
filter[:created_at] = range if range.present?
|
||||
filter[:inbox_id] = params[:inbox_ids] if params[:inbox_ids].present?
|
||||
filter
|
||||
end
|
||||
|
||||
def fetch_grouped_counts
|
||||
label_names = filtered_labels.map(&:title)
|
||||
return {} if label_names.empty?
|
||||
|
||||
ActsAsTaggableOn::Tagging
|
||||
.joins('INNER JOIN conversations ON taggings.taggable_id = conversations.id')
|
||||
.joins('INNER JOIN tags ON taggings.tag_id = tags.id')
|
||||
.where(taggable_type: 'Conversation', context: 'labels', conversations: conversation_filter)
|
||||
.where(tags: { name: label_names })
|
||||
.group('conversations.inbox_id', 'tags.name')
|
||||
.count
|
||||
end
|
||||
|
||||
def build_matrix
|
||||
counts = fetch_grouped_counts
|
||||
filtered_inboxes.map do |inbox|
|
||||
filtered_labels.map do |label|
|
||||
counts[[inbox.id, label.title]] || 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -62,6 +62,14 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
render json: bot_metrics
|
||||
end
|
||||
|
||||
def inbox_label_matrix
|
||||
builder = V2::Reports::InboxLabelMatrixBuilder.new(
|
||||
account: Current.account,
|
||||
params: inbox_label_matrix_params
|
||||
)
|
||||
render json: builder.build
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_csv(filename, template)
|
||||
@@ -139,4 +147,13 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
def conversation_metrics
|
||||
V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics
|
||||
end
|
||||
|
||||
def inbox_label_matrix_params
|
||||
{
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
inbox_ids: params[:inbox_ids],
|
||||
label_ids: params[:label_ids]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -444,6 +444,7 @@ Rails.application.routes.draw do
|
||||
get :conversations_summary
|
||||
get :conversation_traffic
|
||||
get :bot_metrics
|
||||
get :inbox_label_matrix
|
||||
end
|
||||
end
|
||||
resource :year_in_review, only: [:show]
|
||||
|
||||
135
spec/builders/v2/reports/inbox_label_matrix_builder_spec.rb
Normal file
135
spec/builders/v2/reports/inbox_label_matrix_builder_spec.rb
Normal file
@@ -0,0 +1,135 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe V2::Reports::InboxLabelMatrixBuilder do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:inbox_one) { create(:inbox, account: account, name: 'Email Support') }
|
||||
let!(:inbox_two) { create(:inbox, account: account, name: 'Web Chat') }
|
||||
let!(:label_one) { create(:label, account: account, title: 'bug') }
|
||||
let!(:label_two) { create(:label, account: account, title: 'feature') }
|
||||
let(:params) do
|
||||
{
|
||||
since: 1.week.ago.beginning_of_day.to_i.to_s,
|
||||
until: Time.current.end_of_day.to_i.to_s
|
||||
}
|
||||
end
|
||||
let(:builder) { described_class.new(account: account, params: params) }
|
||||
|
||||
describe '#build' do
|
||||
subject(:report) { builder.build }
|
||||
|
||||
context 'when there are conversations with labels across inboxes' do
|
||||
before do
|
||||
c1 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
|
||||
c1.update(label_list: [label_one.title])
|
||||
|
||||
c2 = create(:conversation, account: account, inbox: inbox_one, created_at: 3.days.ago)
|
||||
c2.update(label_list: [label_one.title, label_two.title])
|
||||
|
||||
c3 = create(:conversation, account: account, inbox: inbox_two, created_at: 1.day.ago)
|
||||
c3.update(label_list: [label_two.title])
|
||||
end
|
||||
|
||||
it 'returns inboxes ordered by name' do
|
||||
expect(report[:inboxes]).to eq([
|
||||
{ id: inbox_one.id, name: 'Email Support' },
|
||||
{ id: inbox_two.id, name: 'Web Chat' }
|
||||
])
|
||||
end
|
||||
|
||||
it 'returns labels ordered by title' do
|
||||
expect(report[:labels]).to eq([
|
||||
{ id: label_one.id, title: 'bug' },
|
||||
{ id: label_two.id, title: 'feature' }
|
||||
])
|
||||
end
|
||||
|
||||
it 'returns correct conversation counts in the matrix' do
|
||||
# Email Support: bug=2, feature=1
|
||||
# Web Chat: bug=0, feature=1
|
||||
expect(report[:matrix]).to eq([[2, 1], [0, 1]])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by inbox_ids' do
|
||||
let(:params) do
|
||||
{
|
||||
since: 1.week.ago.beginning_of_day.to_i.to_s,
|
||||
until: Time.current.end_of_day.to_i.to_s,
|
||||
inbox_ids: [inbox_one.id]
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
c1 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
|
||||
c1.update(label_list: [label_one.title])
|
||||
|
||||
c2 = create(:conversation, account: account, inbox: inbox_two, created_at: 1.day.ago)
|
||||
c2.update(label_list: [label_one.title])
|
||||
end
|
||||
|
||||
it 'only includes the specified inboxes and their counts' do
|
||||
expect(report[:inboxes]).to eq([{ id: inbox_one.id, name: 'Email Support' }])
|
||||
expect(report[:matrix]).to eq([[1, 0]])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by label_ids' do
|
||||
let(:params) do
|
||||
{
|
||||
since: 1.week.ago.beginning_of_day.to_i.to_s,
|
||||
until: Time.current.end_of_day.to_i.to_s,
|
||||
label_ids: [label_one.id]
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
c1 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
|
||||
c1.update(label_list: [label_one.title, label_two.title])
|
||||
end
|
||||
|
||||
it 'only includes the specified labels and their counts' do
|
||||
expect(report[:labels]).to eq([{ id: label_one.id, title: 'bug' }])
|
||||
expect(report[:matrix]).to eq([[1], [0]])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversations are outside the date range' do
|
||||
before do
|
||||
c1 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
|
||||
c1.update(label_list: [label_one.title])
|
||||
|
||||
c2 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.weeks.ago)
|
||||
c2.update(label_list: [label_one.title])
|
||||
end
|
||||
|
||||
it 'only counts conversations within the date range' do
|
||||
expect(report[:matrix]).to eq([[1, 0], [0, 0]])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no conversations with labels' do
|
||||
before do
|
||||
create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
|
||||
end
|
||||
|
||||
it 'returns a matrix of zeros' do
|
||||
expect(report[:matrix]).to eq([[0, 0], [0, 0]])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversations belong to another account' do
|
||||
let(:other_account) { create(:account) }
|
||||
let(:other_inbox) { create(:inbox, account: other_account) }
|
||||
|
||||
before do
|
||||
c1 = create(:conversation, account: other_account, inbox: other_inbox, created_at: 2.days.ago)
|
||||
other_label = create(:label, account: other_account, title: 'bug')
|
||||
c1.update(label_list: [other_label.title])
|
||||
end
|
||||
|
||||
it 'does not include conversations from other accounts' do
|
||||
expect(report[:matrix]).to eq([[0, 0], [0, 0]])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -196,4 +196,56 @@ RSpec.describe Api::V2::Accounts::ReportsController, type: :request do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/{account.id}/reports/inbox_label_matrix' do
|
||||
let!(:inbox_one) { create(:inbox, account: account, name: 'Email Support') }
|
||||
let!(:label_one) { create(:label, account: account, title: 'bug') }
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/inbox_label_matrix"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as agent' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/inbox_label_matrix",
|
||||
headers: agent.create_new_auth_token, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as admin' do
|
||||
before do
|
||||
c1 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
|
||||
c1.update(label_list: [label_one.title])
|
||||
end
|
||||
|
||||
it 'returns the inbox label matrix' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/inbox_label_matrix",
|
||||
params: { since: 1.week.ago.to_i.to_s, until: Time.current.to_i.to_s },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
body = response.parsed_body
|
||||
expect(body['inboxes']).to be_an(Array)
|
||||
expect(body['labels']).to be_an(Array)
|
||||
expect(body['matrix']).to be_an(Array)
|
||||
end
|
||||
|
||||
it 'filters by inbox_ids and label_ids' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/inbox_label_matrix",
|
||||
params: { inbox_ids: [inbox_one.id], label_ids: [label_one.id] },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
body = response.parsed_body
|
||||
expect(body['inboxes'].length).to eq(1)
|
||||
expect(body['labels'].length).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user