Allinea i numeri della dashboard tra griglia, funnel e liste di attenzione.
Il funnel mostra stage attuale e cumulato, i KPI escludono Da contattare e le liste usano il totale reale invece del tetto a 20. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
class Dashboard::Metrics
|
||||
FUNNEL_STAGES = (Catalog::OPEN_PIPELINE_STAGES + %w[won]).freeze
|
||||
ATTENTION_PREVIEW = 8
|
||||
|
||||
def initialize(scope: Opportunity.all, project: nil)
|
||||
@scope = scope
|
||||
@project = project
|
||||
@@ -21,7 +24,7 @@ class Dashboard::Metrics
|
||||
end
|
||||
|
||||
def conversion_rates
|
||||
stages = Catalog::OPEN_PIPELINE_STAGES + %w[won]
|
||||
stages = FUNNEL_STAGES
|
||||
rates = {}
|
||||
stages.each_cons(2) do |from, to|
|
||||
from_count = cumulative_reached(from)
|
||||
@@ -32,22 +35,29 @@ class Dashboard::Metrics
|
||||
end
|
||||
|
||||
def funnel_steps
|
||||
counts = {
|
||||
"contacted" => reached_or_beyond("contacted"),
|
||||
"replied" => reached_or_beyond("replied"),
|
||||
"interested" => reached_or_beyond("interested"),
|
||||
"first_use" => reached_or_beyond("first_use"),
|
||||
"won" => @scope.won.count
|
||||
}
|
||||
previous_reached = nil
|
||||
previous_label = nil
|
||||
|
||||
steps = []
|
||||
previous = nil
|
||||
counts.each do |stage, count|
|
||||
rate = previous ? percentage(count, previous) : nil
|
||||
steps << { stage: stage, label: Catalog.label_for(Catalog::PIPELINE_STAGES, stage), count: count, rate: rate }
|
||||
previous = count
|
||||
FUNNEL_STAGES.map do |stage|
|
||||
current = stage_counts[stage].to_i
|
||||
reached = reached_or_beyond(stage)
|
||||
label = Catalog.label_for(Catalog::PIPELINE_STAGES, stage)
|
||||
step = {
|
||||
stage: stage,
|
||||
label: label,
|
||||
current: current,
|
||||
reached: reached
|
||||
}
|
||||
|
||||
if previous_reached&.positive?
|
||||
step[:rate] = percentage(reached, previous_reached)
|
||||
step[:conversion_label] = "#{reached} su #{previous_reached} hanno superato #{previous_label}"
|
||||
end
|
||||
|
||||
previous_reached = reached
|
||||
previous_label = label
|
||||
step
|
||||
end
|
||||
steps
|
||||
end
|
||||
|
||||
def avg_days_to_won
|
||||
@@ -83,17 +93,15 @@ class Dashboard::Metrics
|
||||
end
|
||||
|
||||
def opportunities_without_next_action
|
||||
opportunities_missing_next_action.count
|
||||
in_trattativa_without_next_action.count
|
||||
end
|
||||
|
||||
def demo_to_won_conversion
|
||||
demo = reached_or_beyond("demo_trial")
|
||||
percentage(@scope.won.count, demo)
|
||||
stage_to_won_conversion("demo_trial")
|
||||
end
|
||||
|
||||
def first_use_to_won_conversion
|
||||
first_use = reached_or_beyond("first_use")
|
||||
percentage(@scope.won.count, first_use)
|
||||
stage_to_won_conversion("first_use")
|
||||
end
|
||||
|
||||
def arpa
|
||||
@@ -104,14 +112,16 @@ class Dashboard::Metrics
|
||||
end
|
||||
|
||||
def attention_items
|
||||
overdue = tasks_scope.overdue.includes(:organization, :contact, :assigned_user).ordered
|
||||
|
||||
{
|
||||
to_send_count: @scope.where(send_status: "to_send").count,
|
||||
interested_without_followup: interested_without_followup,
|
||||
stalled_opportunities: stalled_opportunities,
|
||||
overdue_tasks: tasks_scope.overdue.includes(:organization, :contact, :assigned_user).ordered.limit(20),
|
||||
organizations_without_contacts: organizations_scope.left_joins(:contacts).where(contacts: { id: nil }).limit(20),
|
||||
opportunities_without_value: advanced_open_stage.where(estimated_value: [nil, 0]).includes(:organization).limit(20),
|
||||
opportunities_without_next_action: opportunities_missing_next_action.merge(advanced_open_stage).limit(20)
|
||||
interested_without_followup: preview_attention(interested_without_followup_scope.includes(:organization)),
|
||||
stalled_opportunities: preview_attention(stalled_opportunities_scope),
|
||||
overdue_tasks: preview_attention(overdue),
|
||||
organizations_without_contacts: preview_attention(organizations_without_contacts_scope),
|
||||
opportunities_without_value: preview_attention(opportunities_without_value_scope),
|
||||
opportunities_without_next_action: preview_attention(in_trattativa_without_next_action.includes(:organization))
|
||||
}
|
||||
end
|
||||
|
||||
@@ -133,7 +143,7 @@ class Dashboard::Metrics
|
||||
idx = Catalog::PIPELINE_ORDER.index(stage)
|
||||
return 0 unless idx
|
||||
|
||||
stages = Catalog::PIPELINE_ORDER[idx..] - ["lost"]
|
||||
stages = Catalog::PIPELINE_ORDER[idx..].without("lost")
|
||||
@scope.where(pipeline_stage: stages).count
|
||||
end
|
||||
|
||||
@@ -143,25 +153,49 @@ class Dashboard::Metrics
|
||||
((part.to_f / whole) * 100).round
|
||||
end
|
||||
|
||||
def interested_without_followup
|
||||
base = @scope.where(pipeline_stage: "interested")
|
||||
with_pending = Task.pending.where.not(opportunity_id: nil).select(:opportunity_id)
|
||||
base.where.not(id: with_pending).includes(:organization).limit(20)
|
||||
def stage_to_won_conversion(stage)
|
||||
return nil if stage_counts[stage].to_i.zero? && @scope.won.count.zero?
|
||||
|
||||
reached = reached_or_beyond(stage)
|
||||
return nil if reached.zero?
|
||||
|
||||
percentage(@scope.won.count, reached)
|
||||
end
|
||||
|
||||
def stalled_opportunities
|
||||
def interested_without_followup_scope
|
||||
@scope.where(pipeline_stage: "interested").where.not(id: pending_opportunity_ids)
|
||||
end
|
||||
|
||||
def stalled_opportunities_scope
|
||||
@scope.open_stage
|
||||
.where("stage_changed_at < ? OR (stage_changed_at IS NULL AND opportunities.created_at < ?)", 7.days.ago, 7.days.ago)
|
||||
.includes(:organization, :assigned_user)
|
||||
.limit(20)
|
||||
end
|
||||
|
||||
def opportunities_missing_next_action
|
||||
with_pending = Task.pending.where.not(opportunity_id: nil).select(:opportunity_id)
|
||||
@scope.open_stage.where.not(id: with_pending).includes(:organization)
|
||||
def organizations_without_contacts_scope
|
||||
organizations_scope.left_joins(:contacts).where(contacts: { id: nil })
|
||||
end
|
||||
|
||||
def opportunities_without_value_scope
|
||||
advanced_open_stage.where("estimated_value IS NULL OR estimated_value = 0").includes(:organization)
|
||||
end
|
||||
|
||||
def in_trattativa_without_next_action
|
||||
advanced_open_stage.where.not(id: pending_opportunity_ids)
|
||||
end
|
||||
|
||||
def pending_opportunity_ids
|
||||
Task.pending.where.not(opportunity_id: nil).select(:opportunity_id)
|
||||
end
|
||||
|
||||
def advanced_open_stage
|
||||
@scope.open_stage.where.not(pipeline_stage: "to_contact")
|
||||
end
|
||||
|
||||
def preview_attention(relation)
|
||||
total = relation.unscope(:includes, :preload, :eager_load, :order, :limit).count
|
||||
total = total.size if total.is_a?(Hash)
|
||||
|
||||
{ items: relation.limit(ATTENTION_PREVIEW).to_a, total: total }
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user