Admin analytics: heatmap per device, anteprima staff e analisi costi.
Separa heatmap mobile/desktop, opt-out analytics per operatori, dashboard costi con KPI e trend mensili. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class CostAnalytics
|
||||
TREND_MONTHS = 12
|
||||
|
||||
def initialize(month:)
|
||||
@month = month.to_date.beginning_of_month
|
||||
end
|
||||
|
||||
def summary
|
||||
build_period(@month)
|
||||
end
|
||||
|
||||
def trend(months: TREND_MONTHS)
|
||||
start_month = @month - (months - 1).months
|
||||
months_list = (0...months).map { |i| start_month + i.months }
|
||||
months_list.map { |m| build_period(m) }
|
||||
end
|
||||
|
||||
def club_breakdown
|
||||
range = month_range(@month)
|
||||
rows = session_rows(range)
|
||||
total_secs = rows.sum { |row| row.total_secs.to_i }
|
||||
total_cost_cents = PlatformCostEntry.for_month(@month).sum(:amount_cents)
|
||||
revenue_by_club = revenue_by_club(range)
|
||||
storage_by_club = storage_by_club_index
|
||||
club_ids = rows.map(&:club_id)
|
||||
clubs = Club.where(id: club_ids).includes(subscription: :plan).index_by(&:id)
|
||||
|
||||
rows.map do |row|
|
||||
club = clubs[row.club_id]
|
||||
secs = row.total_secs.to_i
|
||||
hours = secs / 3600.0
|
||||
sessions_count = row.sessions_count.to_i
|
||||
share = total_secs.positive? ? secs.to_f / total_secs : 0.0
|
||||
allocated_cents = (total_cost_cents * share).round
|
||||
revenue_cents = revenue_by_club[row.club_id].to_i
|
||||
storage_bytes = storage_by_club[row.club_id].to_i
|
||||
|
||||
{
|
||||
club_id: row.club_id,
|
||||
club_name: row.club_name,
|
||||
plan_slug: club&.subscription&.plan&.slug,
|
||||
sessions: sessions_count,
|
||||
hours: hours.round(2),
|
||||
hours_share_pct: (share * 100).round(1),
|
||||
allocated_cost_cents: allocated_cents,
|
||||
revenue_cents: revenue_cents,
|
||||
margin_cents: revenue_cents - allocated_cents,
|
||||
cost_per_hour_cents: hours.positive? ? (allocated_cents / hours).round : nil,
|
||||
cost_per_session_cents: sessions_count.positive? ? (allocated_cents / sessions_count) : nil,
|
||||
storage_bytes: storage_bytes
|
||||
}
|
||||
end.sort_by { |row| [-row[:hours], row[:club_name]] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_period(month)
|
||||
range = month_range(month)
|
||||
cost_cents = PlatformCostEntry.for_month(month).sum(:amount_cents)
|
||||
sessions_scope = ended_sessions.where(ended_at: range)
|
||||
total_secs = sessions_scope.sum(:total_duration_secs).to_i
|
||||
hours = total_secs / 3600.0
|
||||
sessions = sessions_scope.count
|
||||
clubs_active = distinct_active_clubs(range)
|
||||
revenue_cents = Billing::Payment.where(status: "paid", paid_at: range).sum(:amount_cents)
|
||||
storage_bytes = Recording.ready.sum(:byte_size).to_i
|
||||
storage_gb = storage_bytes.positive? ? storage_bytes / (1024.0**3) : 0.0
|
||||
|
||||
kpis = compute_kpis(
|
||||
cost_cents: cost_cents,
|
||||
hours: hours,
|
||||
sessions: sessions,
|
||||
clubs_active: clubs_active,
|
||||
revenue_cents: revenue_cents,
|
||||
storage_gb: storage_gb
|
||||
)
|
||||
|
||||
{
|
||||
month: month,
|
||||
cost_cents: cost_cents,
|
||||
sessions: sessions,
|
||||
hours: hours.round(2),
|
||||
clubs_active: clubs_active,
|
||||
revenue_cents: revenue_cents,
|
||||
storage_bytes: storage_bytes,
|
||||
**kpis
|
||||
}
|
||||
end
|
||||
|
||||
def compute_kpis(cost_cents:, hours:, sessions:, clubs_active:, revenue_cents:, storage_gb:)
|
||||
margin_cents = revenue_cents.to_i - cost_cents.to_i
|
||||
margin_pct = revenue_cents.to_i.positive? ? ((margin_cents.to_f / revenue_cents.to_i) * 100).round(1) : nil
|
||||
hours_f = hours.to_f
|
||||
sessions_i = sessions.to_i
|
||||
clubs_i = clubs_active.to_i
|
||||
storage_f = storage_gb.to_f
|
||||
cost_i = cost_cents.to_i
|
||||
|
||||
{
|
||||
cost_per_hour_cents: hours_f.positive? ? (cost_i / hours_f).round : nil,
|
||||
cost_per_session_cents: sessions_i.positive? ? (cost_i / sessions_i) : nil,
|
||||
cost_per_club_cents: clubs_i.positive? ? (cost_i / clubs_i) : nil,
|
||||
revenue_per_hour_cents: hours_f.positive? ? (revenue_cents.to_i / hours_f).round : nil,
|
||||
cost_per_gb_cents: storage_f.positive? ? (cost_i / storage_f).round : nil,
|
||||
margin_cents: margin_cents,
|
||||
margin_pct: margin_pct
|
||||
}
|
||||
end
|
||||
|
||||
def month_range(month)
|
||||
month.beginning_of_month.beginning_of_day..month.end_of_month.end_of_day
|
||||
end
|
||||
|
||||
def ended_sessions
|
||||
StreamSession.where(status: "ended")
|
||||
end
|
||||
|
||||
def distinct_active_clubs(range)
|
||||
ended_sessions
|
||||
.where(ended_at: range)
|
||||
.joins(match: { team: :club })
|
||||
.distinct
|
||||
.count("clubs.id")
|
||||
end
|
||||
|
||||
def session_rows(range)
|
||||
ended_sessions
|
||||
.where(ended_at: range)
|
||||
.joins(match: { team: :club })
|
||||
.group("clubs.id", "clubs.name")
|
||||
.select(
|
||||
"clubs.id AS club_id",
|
||||
"clubs.name AS club_name",
|
||||
"COUNT(stream_sessions.id) AS sessions_count",
|
||||
"SUM(stream_sessions.total_duration_secs) AS total_secs"
|
||||
)
|
||||
end
|
||||
|
||||
def revenue_by_club(range)
|
||||
Billing::Payment.where(status: "paid", paid_at: range).group(:club_id).sum(:amount_cents)
|
||||
end
|
||||
|
||||
def storage_by_club_index
|
||||
Recording.ready.joins(:team).group("teams.club_id").sum(:byte_size)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Analytics
|
||||
module Suppress
|
||||
COOKIE_NAME = "mltv_analytics_suppress"
|
||||
COOKIE_MAX_AGE = 7 * 24 * 60 * 60
|
||||
|
||||
module_function
|
||||
|
||||
def active?(cookie_value)
|
||||
cookie_value.to_s == "1"
|
||||
end
|
||||
|
||||
def enable!(cookie_jar)
|
||||
cookie_jar[COOKIE_NAME] = cookie_options(value: "1", expires: COOKIE_MAX_AGE.seconds.from_now)
|
||||
end
|
||||
|
||||
def disable!(cookie_jar)
|
||||
cookie_jar.delete(
|
||||
COOKIE_NAME,
|
||||
path: "/",
|
||||
same_site: :lax,
|
||||
secure: cookie_secure?
|
||||
)
|
||||
end
|
||||
|
||||
def cookie_options(value:, expires:)
|
||||
{
|
||||
value: value,
|
||||
expires: expires,
|
||||
path: "/",
|
||||
httponly: true,
|
||||
same_site: :lax,
|
||||
secure: cookie_secure?
|
||||
}
|
||||
end
|
||||
|
||||
def cookie_secure?
|
||||
Rails.application.config.force_ssl || Rails.env.production?
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user