feat(reports): filtro por inbox no Relatório do Bot

Hoje as métricas e séries do BotReports agregam toda a conta — não dá pra ver
"a Jasmine da PrimeAL está errando mais que a do Qnn01". Cada unidade tem
prompt próprio, então um sintoma localizado precisa de medição localizada.

Backend:
- Inbox#has_many :reporting_events (relação inversa que faltava)
- BotMetricsBuilder aceita inbox_id e filtra bot_conversations + base_reporting_events
- bot_metrics endpoint passa inbox_id pelos params permitidos
- count_report_builder já suporta scope=inbox; agora funciona pra
  bot_resolutions_count e bot_handoffs_count graças à relação acima

Frontend:
- BotReports.vue: ReportFilters com filter-type='inboxes' e dropdown ativo
- Quando uma inbox é escolhida, requestPayload inclui inboxId/type/id e os
  fetches (BotMetrics + ReportContainer) passam o filtro
- API client getBotMetrics aceita inboxId; getBotSummary aceita type+id
- Sem inbox selecionada: comportamento antigo (agregação da conta)

Bonus na rake task de retroativo:
- rebuild_bot_resolved.rake: Message.unscope(:order) pra evitar conflito
  PG::InvalidColumnReference (DISTINCT + ORDER BY default scope)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rodribm10 2026-04-26 11:47:46 -03:00
parent 7cd2ea1258
commit e9b8b6e587
7 changed files with 55 additions and 16 deletions

View File

@ -18,8 +18,15 @@ class V2::Reports::BotMetricsBuilder
private
def filter_inbox_id
@filter_inbox_id ||= params[:inbox_id].presence&.to_i
end
def bot_activated_inbox_ids
@bot_activated_inbox_ids ||= account.inboxes.filter(&:active_bot?).map(&:id)
@bot_activated_inbox_ids ||= begin
ids = account.inboxes.filter(&:active_bot?).map(&:id)
filter_inbox_id ? ids & [filter_inbox_id] : ids
end
end
def bot_conversations
@ -30,14 +37,18 @@ class V2::Reports::BotMetricsBuilder
@bot_messages ||= account.messages.outgoing.where(conversation_id: bot_conversations.ids).where(created_at: range)
end
def base_reporting_events
scope = account.reporting_events.where(account_id: account.id, created_at: range)
scope = scope.where(inbox_id: filter_inbox_id) if filter_inbox_id
scope
end
def bot_resolutions_count
account.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_resolved,
created_at: range).distinct.count
base_reporting_events.joins(:conversation).select(:conversation_id).where(name: :conversation_bot_resolved).distinct.count
end
def bot_handoffs_count
account.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_handoff,
created_at: range).distinct.count
base_reporting_events.joins(:conversation).select(:conversation_id).where(name: :conversation_bot_handoff).distinct.count
end
def bot_resolution_rate

View File

@ -1,3 +1,4 @@
# rubocop:disable Metrics/ClassLength
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
include Api::V2::Accounts::ReportsHelper
include Api::V2::Accounts::HeatmapHelper
@ -58,7 +59,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
end
def bot_metrics
bot_metrics = V2::Reports::BotMetricsBuilder.new(Current.account, params).metrics
bot_metrics = V2::Reports::BotMetricsBuilder.new(Current.account, bot_metrics_params).metrics
render json: bot_metrics
end
@ -196,6 +197,14 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
}
end
def bot_metrics_params
{
inbox_id: params[:inbox_id],
since: params[:since],
until: params[:until]
}
end
def inbox_leads_summary_params
{
inbox_id: params[:inbox_id],
@ -205,3 +214,4 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
}
end
end
# rubocop:enable Metrics/ClassLength

View File

@ -91,18 +91,19 @@ class ReportsAPI extends ApiClient {
});
}
getBotMetrics({ from, to } = {}) {
getBotMetrics({ from, to, inboxId } = {}) {
return axios.get(`${this.url}/bot_metrics`, {
params: { since: from, until: to },
params: { since: from, until: to, inbox_id: inboxId },
});
}
getBotSummary({ from, to, groupBy, businessHours } = {}) {
getBotSummary({ from, to, groupBy, businessHours, type, id } = {}) {
return axios.get(`${this.url}/bot_summary`, {
params: {
since: from,
until: to,
type: 'account',
type: type || 'account',
id,
group_by: groupBy,
business_hours: businessHours,
},

View File

@ -20,6 +20,7 @@ export default {
from: 0,
to: 0,
groupBy: GROUP_BY_FILTER[1],
inboxId: null,
reportKeys: {
BOT_RESOLUTION_COUNT: 'bot_resolutions_count',
BOT_HANDOFF_COUNT: 'bot_handoffs_count',
@ -32,6 +33,7 @@ export default {
return {
from: this.from,
to: this.to,
inboxId: this.inboxId,
};
},
},
@ -60,24 +62,35 @@ export default {
});
},
getRequestPayload() {
const { from, to, groupBy, businessHours } = this;
return {
const { from, to, groupBy, businessHours, inboxId } = this;
const payload = {
from,
to,
groupBy: groupBy?.period,
businessHours,
};
if (inboxId) {
payload.type = 'inbox';
payload.id = inboxId;
}
return payload;
},
onFilterChange({ from, to, groupBy, businessHours }) {
onFilterChange({ from, to, groupBy, businessHours, inboxes }) {
this.from = from;
this.to = to;
this.groupBy = groupBy;
this.businessHours = businessHours;
this.inboxId = inboxes?.id || null;
this.fetchAllData();
useTrack(REPORTS_EVENTS.FILTER_REPORT, {
filterValue: { from, to, groupBy, businessHours },
filterValue: {
from,
to,
groupBy,
businessHours,
inboxId: this.inboxId,
},
reportType: 'bots',
});
},
@ -89,7 +102,7 @@ export default {
<ReportHeader :header-title="$t('BOT_REPORTS.HEADER')" />
<div class="flex flex-col gap-4">
<ReportFilters
:show-entity-filter="false"
filter-type="inboxes"
show-group-by
:show-business-hours="false"
@filter-change="onFilterChange"

View File

@ -180,6 +180,8 @@ export const actions = {
to: reportObj.to,
groupBy: reportObj.groupBy,
businessHours: reportObj.businessHours,
type: reportObj.type,
id: reportObj.id,
})
.then(botSummary => {
commit(types.default.SET_BOT_SUMMARY, botSummary.data);

View File

@ -77,6 +77,7 @@ class Inbox < ApplicationRecord
has_many :conversations, dependent: :destroy_async
has_many :messages, dependent: :destroy_async
has_many :scheduled_messages, dependent: :destroy_async
has_many :reporting_events, dependent: :nullify
has_one :inbox_assignment_policy, dependent: :destroy
has_one :assignment_policy, through: :inbox_assignment_policy

View File

@ -38,6 +38,7 @@ namespace :reports do
base_scope = base_scope.where(account_id: account_id) if account_id
affected_conversation_ids = Message
.unscope(:order)
.where(message_type: :outgoing, sender_id: nil)
.distinct
.pluck(:conversation_id)