iachat/app/builders/v2/reports/inbox_benchmarking_builder.rb
Rodribm10 d831ee4d33 feat(reports): Painel Diretoria — Onda 1A (leitura)
Primeira onda do roadmap de indicadores executivos do Grupo Nova. Mede
ADOÇÃO DO CANAL DIGITAL, não a operação total — banner explícito alerta
que reservas fechadas manualmente na recepção ainda não estão capturadas
(Onda 1B vai adicionar marcação manual via botão na conversa).

Backend:
- V2::Reports::ConversionFunnelBuilder — leads (novo/retorno/total),
  reservas (criadas != draft, pagas in active/completed/confirmed),
  taxas de conversão. Filtro opcional por inbox.
- V2::Reports::InboxBenchmarkingBuilder — uma linha por inbox com
  brand_name (via Captain::UnitInbox -> Unit -> Brand)
- Endpoints GET /reports/conversion_funnel e /reports/inbox_benchmarking
- RSpec do ConversionFunnelBuilder

Frontend:
- Rota top-level Reports → Painel Diretoria
- DirectoryDashboard.vue: banner de adoção + filtros + cards + funil + tabela
  benchmarking agrupada por marca com variação vs média
- API client getConversionFunnel + getInboxBenchmarking
- i18n EN + PT

Memórias suporte: feedback_metricas_adocao_canal.md + project_painel_diretoria_roadmap.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:44:59 -03:00

80 lines
2.6 KiB
Ruby

class V2::Reports::InboxBenchmarkingBuilder
include DateRangeHelper
PAID_STATUSES = V2::Reports::ConversionFunnelBuilder::PAID_STATUSES
IGNORED_CREATED_STATUSES = V2::Reports::ConversionFunnelBuilder::IGNORED_CREATED_STATUSES
attr_reader :account, :params
def initialize(account, params)
@account = account
@params = params
end
# Returns one row per inbox of the account, with leads + reservations + rate,
# plus the brand name so the frontend can group by brand for benchmarking.
def build
return [] if range.blank?
inbox_brand_lookup = build_inbox_brand_lookup
account.inboxes.map do |inbox|
brand_name = inbox_brand_lookup[inbox.id]
leads_total = leads_count_by_inbox[inbox.id] || 0
reservations_created = reservations_created_by_inbox[inbox.id] || 0
reservations_paid = reservations_paid_by_inbox[inbox.id] || 0
{
inbox_id: inbox.id,
inbox_name: inbox.name,
brand_name: brand_name,
leads_total: leads_total,
reservations_created: reservations_created,
reservations_paid: reservations_paid,
conversion_rate: percent(reservations_paid, leads_total)
}
end
end
private
def leads_count_by_inbox
@leads_count_by_inbox ||= account.conversations
.where(created_at: range)
.group(:inbox_id)
.count
end
def reservations_created_by_inbox
@reservations_created_by_inbox ||= account.captain_reservations
.where(created_at: range)
.where.not(status: IGNORED_CREATED_STATUSES)
.group(:inbox_id)
.count
end
def reservations_paid_by_inbox
@reservations_paid_by_inbox ||= account.captain_reservations
.where(created_at: range, status: PAID_STATUSES)
.group(:inbox_id)
.count
end
# inbox_id => brand_name (or nil when there is no brand mapped for this inbox)
def build_inbox_brand_lookup
rows = Captain::UnitInbox
.joins(captain_unit: :brand)
.where(inbox_id: account.inboxes.select(:id))
.pluck('captain_unit_inboxes.inbox_id, captain_brands.name')
rows.to_h
end
def percent(numerator, denominator)
return 0 if denominator.to_i.zero?
(numerator.to_f / denominator * 100).round(1)
end
end