class Dashboard::Metrics def initialize(scope: Opportunity.all, project: nil) @scope = scope @project = project end def stage_counts @stage_counts ||= Catalog::PIPELINE_ORDER.index_with { |stage| @scope.where(pipeline_stage: stage).count } end def prospect_count organizations_scope.prospects.count end def open_pipeline_value @scope.open_stage.sum(:estimated_value).to_f end def won_value @scope.won.sum(:estimated_value).to_f end def conversion_rates stages = Catalog::OPEN_PIPELINE_STAGES + %w[won] rates = {} stages.each_cons(2) do |from, to| from_count = cumulative_reached(from) to_count = cumulative_reached(to) rates["#{from}_to_#{to}"] = percentage(to_count, from_count) end rates 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 } 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 end steps end def avg_days_to_won records = @scope.won.where.not(first_contacted_at: nil, won_at: nil) return nil if records.empty? total = records.sum { |o| ((o.won_at - o.first_contacted_at) / 1.day) } (total / records.size).round(1) end def avg_days_since_last_activity orgs = organizations_scope.left_joins(:activities) .select("organizations.id, MAX(activities.happened_at) AS last_at") .group("organizations.id") .having("MAX(activities.happened_at) IS NOT NULL") return nil if orgs.empty? days = orgs.map { |o| ((Time.current - o.last_at) / 1.day) } (days.sum / days.size).round(1) end def prospects_without_activity_over_7_days organizations_scope.prospects .left_joins(:activities) .group("organizations.id") .having("MAX(activities.happened_at) IS NULL OR MAX(activities.happened_at) < ?", 7.days.ago) .count .size end def overdue_tasks_count tasks_scope.overdue.count end def opportunities_without_next_action opportunities_missing_next_action.count end def demo_to_won_conversion demo = reached_or_beyond("demo_trial") percentage(@scope.won.count, demo) end def first_use_to_won_conversion first_use = reached_or_beyond("first_use") percentage(@scope.won.count, first_use) end def arpa won = @scope.won.where.not(estimated_value: nil) return 0 if won.empty? (won.sum(:estimated_value).to_f / won.count).round(2) end def attention_items { 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) } end private def organizations_scope @project ? Organization.for_project(@project) : Organization.all end def tasks_scope @project ? Task.for_project(@project) : Task.all end def cumulative_reached(stage) reached_or_beyond(stage) end def reached_or_beyond(stage) idx = Catalog::PIPELINE_ORDER.index(stage) return 0 unless idx stages = Catalog::PIPELINE_ORDER[idx..] - ["lost"] @scope.where(pipeline_stage: stages).count end def percentage(part, whole) return 0 if whole.to_i.zero? ((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) end def stalled_opportunities @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) end def advanced_open_stage @scope.open_stage.where.not(pipeline_stage: "to_contact") end end