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 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 = FUNNEL_STAGES 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 previous_reached = nil previous_label = nil 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 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 in_trattativa_without_next_action.count end def demo_to_won_conversion stage_to_won_conversion("demo_trial") end def first_use_to_won_conversion stage_to_won_conversion("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 overdue = tasks_scope.overdue.includes(:organization, :contact, :assigned_user).ordered { to_send_count: @scope.where(send_status: "to_send").count, 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 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..].without("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 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 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) end 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