diff --git a/app/controllers/api/v1/accounts/reports/assignment_metrics_controller.rb b/app/controllers/api/v1/accounts/reports/assignment_metrics_controller.rb deleted file mode 100644 index 96c88dfd8..000000000 --- a/app/controllers/api/v1/accounts/reports/assignment_metrics_controller.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Accounts::Reports::AssignmentMetricsController < Api::V1::Accounts::BaseController - include AssignmentMetricsHelper - include DistributionMetrics - - before_action :check_authorization - before_action :validate_date_range - - def index - @metrics = metrics_service.compute_assignment_metrics - render json: { assignment_metrics: @metrics } - end - - def agent_history - history_service = Reports::AgentHistoryService.new(Current.account, params) - - if params[:agent_id].present? - @agent = Current.account.users.find(params[:agent_id]) - @assignment_history = history_service.fetch_agent_assignment_history(@agent) - - render json: { - agent: serialize_agent(@agent), - assignment_history: @assignment_history, - meta: pagination_meta - } - else - agents_summary = history_service.compute_all_agents_history - render json: { agents_history: agents_summary } - end - end - - def policy_performance - @policies = Current.account.assignment_policies.includes(:inboxes) - performance_data = @policies.map do |policy| - metrics_service.compute_policy_performance(policy) - end - - render json: { policy_performance: performance_data } - end - - def agent_utilization - agents = Current.account.users.joins(:account_users).where(account_users: { role: %w[agent administrator] }) - utilization_data = agents.map do |agent| - metrics_service.compute_agent_utilization(agent) - end - - render json: { agent_utilization: utilization_data } - end - - def assignment_distribution - distribution_data = { - by_inbox: metrics_service.compute_distribution_by_inbox, - by_team: metrics_service.compute_distribution_by_team, - by_hour: metrics_service.compute_distribution_by_hour, - by_day_of_week: compute_distribution_by_day_of_week - } - - render json: { assignment_distribution: distribution_data } - end - - def export - type = params[:type] || 'csv' - data = metrics_service.compute_assignment_metrics - - case type - when 'csv' - export_service = Reports::AssignmentExportService.new(data) - send_data export_service.generate_csv, filename: "assignment_metrics_#{Date.current}.csv", type: 'text/csv' - when 'json' - send_data data.to_json, filename: "assignment_metrics_#{Date.current}.json", type: 'application/json' - else - render json: { error: 'Unsupported export type' }, status: :bad_request - end - end - - private - - def validate_date_range - return if params[:since].blank? || params[:until].blank? - - start_date = Date.parse(params[:since]) - end_date = Date.parse(params[:until]) - - if start_date > end_date - render json: { error: 'Start date must be before end date' }, status: :unprocessable_entity - elsif (end_date - start_date).to_i > 365 - render json: { error: 'Date range cannot exceed 365 days' }, status: :unprocessable_entity - end - rescue Date::Error - render json: { error: 'Invalid date format' }, status: :unprocessable_entity - end - - def check_authorization - authorize Current.account, :show_metrics? - end - - def metrics_service - @metrics_service ||= Reports::AssignmentMetricsService.new(Current.account, params) - end -end diff --git a/app/controllers/concerns/assignment_metrics_helper.rb b/app/controllers/concerns/assignment_metrics_helper.rb deleted file mode 100644 index 8229c3231..000000000 --- a/app/controllers/concerns/assignment_metrics_helper.rb +++ /dev/null @@ -1,117 +0,0 @@ -# frozen_string_literal: true - -module AssignmentMetricsHelper - extend ActiveSupport::Concern - - private - - def compute_summary_metrics - conversations = filter_conversations_by_date_range - - { - total_assignments: conversations.count, - average_assignment_time: calculate_average_assignment_time(conversations), - assignments_per_agent: calculate_assignments_per_agent(conversations), - unassigned_conversations: Current.account.conversations.open.unassigned.count, - policies_active: Current.account.assignment_policies.enabled.count - } - end - - def compute_inbox_metrics - inboxes = params[:inbox_id].present? ? Current.account.inboxes.where(id: params[:inbox_id]) : Current.account.inboxes - - inboxes.map do |inbox| - conversations = filter_conversations_by_date_range.where(inbox_id: inbox.id) - { - inbox_id: inbox.id, - inbox_name: inbox.name, - total_assignments: conversations.count, - average_assignment_time: calculate_average_assignment_time(conversations), - unique_agents: conversations.where.not(assignee_id: nil).distinct.count(:assignee_id), - assignment_policy: inbox.assignment_policy&.name - } - end - end - - def compute_agent_metrics - agents = Current.account.users.joins(:account_users).where(account_users: { role: %w[agent administrator] }) - - agents.map do |agent| - conversations = filter_conversations_by_date_range.where(assignee_id: agent.id) - { - agent_id: agent.id, - agent_name: agent.name, - agent_email: agent.email, - assignment_count: conversations.count, - average_resolution_time: calculate_average_resolution_time(conversations), - current_load: agent.assigned_conversations.open.count, - capacity_utilization: compute_agent_capacity_utilization(agent) - } - end - end - - def compute_policy_metrics - Current.account.assignment_policies.enabled.map do |policy| - compute_policy_performance(policy) - end - end - - def filter_conversations_by_date_range - Current.account.conversations.where(created_at: date_range) - end - - def date_range - @date_range ||= params[:since]..params[:until] - end - - def calculate_average_assignment_time(_conversations) - # Implementation - 0 - end - - def calculate_assignments_per_agent(conversations) - agents_count = conversations.where.not(assignee_id: nil).distinct.count(:assignee_id) - return 0 if agents_count.zero? - - (conversations.count.to_f / agents_count).round(2) - end - - def calculate_average_resolution_time(_conversations) - # Implementation - 0 - end - - def compute_agent_capacity_utilization(_agent) - # Implementation - 0.0 - end - - def calculate_assignment_success_rate(_conversations) - # Implementation - 100.0 - end - - def serialize_agent(agent) - { - id: agent.id, - name: agent.name, - email: agent.email - } - end - - def group_by_hour(conversations) - conversations.group_by_hour(:created_at).count - end - - def group_by_day(conversations) - conversations.group_by_day(:created_at).count - end - - def group_by_week(conversations) - conversations.group_by_week(:created_at).count - end - - def group_by_month(conversations) - conversations.group_by_month(:created_at).count - end -end diff --git a/app/controllers/concerns/distribution_metrics.rb b/app/controllers/concerns/distribution_metrics.rb deleted file mode 100644 index d258806b8..000000000 --- a/app/controllers/concerns/distribution_metrics.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -module DistributionMetrics - extend ActiveSupport::Concern - - def compute_distribution_by_day_of_week - filter_conversations_by_date_range - .group_by_day_of_week(:created_at) - .count - end - - def compute_policy_performance(policy) - inbox_ids = policy.inboxes.pluck(:id) - conversations = filter_conversations_by_date_range.where(inbox_id: inbox_ids) - - { - policy_id: policy.id, - policy_name: policy.name, - assignment_order: policy.assignment_order, - total_assignments: conversations.count, - average_assignment_time: calculate_average_assignment_time(conversations), - success_rate: calculate_assignment_success_rate(conversations), - inbox_count: inbox_ids.count - } - end - - def compute_agent_utilization(agent) - capacity_policy = agent.account_users.first&.agent_capacity_policy - assigned_conversations = agent.assigned_conversations.open.count - - utilization = if capacity_policy - policy_limit = capacity_policy.inbox_capacity_limits.sum(:conversation_limit) - policy_limit.positive? ? (assigned_conversations.to_f / policy_limit * 100).round(2) : 0 - else - 0 - end - - { - agent: serialize_agent(agent), - assigned_conversations: assigned_conversations, - capacity_policy: capacity_policy&.name, - utilization_percentage: utilization, - available_capacity: capacity_policy ? capacity_policy.inbox_capacity_limits.sum(:conversation_limit) - assigned_conversations : nil - } - end - - def compute_distribution_by_inbox - filter_conversations_by_date_range - .joins(:inbox) - .group('inboxes.id', 'inboxes.name') - .count - .map { |k, v| { inbox_id: k[0], inbox_name: k[1], count: v } } - end - - def compute_distribution_by_team - filter_conversations_by_date_range - .joins(assignee: { team_members: :team }) - .group('teams.id', 'teams.name') - .count - .map { |k, v| { team_id: k[0], team_name: k[1], count: v } } - end - - def compute_distribution_by_hour - filter_conversations_by_date_range - .group_by_hour_of_day(:created_at) - .count - end - - def calculate_average_resolution_time(conversations) - resolved = conversations.resolved - return 0 if resolved.empty? - - total_time = resolved.sum { |c| (c.last_activity_at - c.created_at) / 1.hour } - (total_time / resolved.count).round(2) - end - - def calculate_assignment_success_rate(conversations) - total = conversations.count - return 0.0 if total.zero? - - successful = conversations.where(status: %w[resolved snoozed]).count - (successful.to_f / total * 100).round(2) - end - - def pagination_meta - { - current_page: params[:page] || 1, - per_page: params[:per_page] || 50, - total_count: @assignment_history&.total_count || 0 - } - end -end diff --git a/app/services/reports/agent_history_service.rb b/app/services/reports/agent_history_service.rb deleted file mode 100644 index 9138f66a9..000000000 --- a/app/services/reports/agent_history_service.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true - -class Reports::AgentHistoryService - attr_reader :account, :params - - def initialize(account, params = {}) - @account = account - @params = params - end - - def fetch_agent_assignment_history(agent) - conversations = agent.assigned_conversations - .includes(:inbox, :contact) - .where(created_at: date_range) - .order(created_at: :desc) - .page(params[:page]) - .per(params[:per_page] || 50) - - conversations.map do |conversation| - { - conversation_id: conversation.id, - inbox_id: conversation.inbox_id, - inbox_name: conversation.inbox.name, - contact_name: conversation.contact.name, - assigned_at: conversation.assignee_last_seen_at || conversation.created_at, - status: conversation.status, - created_at: conversation.created_at - } - end - end - - def compute_all_agents_history - agents = account.users.joins(:account_users).where(account_users: { role: %w[agent administrator] }) - - agents.map do |agent| - conversations = agent.assigned_conversations.where(created_at: date_range) - { - agent: serialize_agent(agent), - total_assignments: conversations.count, - resolved_count: conversations.resolved.count, - open_count: conversations.open.count, - average_resolution_time: calculate_average_resolution_time(conversations.resolved) - } - end - end - - private - - def date_range - start_date = params[:start_date] ? Date.parse(params[:start_date]).beginning_of_day : 30.days.ago - end_date = params[:end_date] ? Date.parse(params[:end_date]).end_of_day : Time.current - start_date..end_date - end - - def serialize_agent(agent) - { - id: agent.id, - name: agent.name, - email: agent.email, - avatar_url: agent.avatar_url - } - end - - def calculate_average_resolution_time(conversations) - return 0 if conversations.empty? - - total_time = conversations.sum { |c| (c.last_activity_at - c.created_at) / 1.hour } - (total_time / conversations.count).round(2) - end -end diff --git a/app/services/reports/assignment_export_service.rb b/app/services/reports/assignment_export_service.rb deleted file mode 100644 index 58a6e7ccb..000000000 --- a/app/services/reports/assignment_export_service.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -require 'csv' - -class Reports::AssignmentExportService - attr_reader :data - - def initialize(data) - @data = data - end - - def generate_csv - CSV.generate(headers: true) do |csv| - add_header(csv) - add_summary_metrics(csv) - add_inbox_metrics(csv) - add_agent_metrics(csv) - end - end - - private - - def add_header(csv) - csv << ['Assignment Metrics Report'] - csv << ['Generated at', Time.current] - csv << [] - end - - def add_summary_metrics(csv) - csv << ['Summary Metrics'] - csv << %w[Metric Value] - data[:summary].each do |key, value| - csv << [key.to_s.humanize, value] - end - csv << [] - end - - def add_inbox_metrics(csv) - csv << ['Inbox Metrics'] - csv << ['Inbox Name', 'Total Assignments', 'Average Assignment Time', 'Unique Agents'] - data[:by_inbox].each do |inbox| - csv << [inbox[:inbox_name], inbox[:total_assignments], inbox[:average_assignment_time], inbox[:unique_agents]] - end - csv << [] - end - - def add_agent_metrics(csv) - csv << ['Agent Metrics'] - csv << ['Agent Name', 'Email', 'Assignment Count'] - data[:by_agent].each do |agent| - csv << [agent[:agent_name], agent[:agent_email], agent[:assignment_count]] - end - end -end diff --git a/app/services/reports/assignment_metrics_service.rb b/app/services/reports/assignment_metrics_service.rb deleted file mode 100644 index d44ed8a9d..000000000 --- a/app/services/reports/assignment_metrics_service.rb +++ /dev/null @@ -1,163 +0,0 @@ -# frozen_string_literal: true - -class Reports::AssignmentMetricsService - attr_reader :account, :params - - def initialize(account, params = {}) - @account = account - @params = params - end - - def compute_assignment_metrics - { - total_assigned: total_assigned_conversations, - assignment_rate: calculate_assignment_rate, - average_response_time: calculate_average_response_time, - average_resolution_time: calculate_average_resolution_time, - assignments_by_policy: assignments_by_policy, - period_metrics: compute_period_metrics - } - end - - def compute_policy_performance(policy) - conversations = policy.assignment_logs - .joins(:conversation) - .where(conversations: { created_at: date_range }) - - { - policy_id: policy.id, - policy_name: policy.name, - total_assignments: conversations.count, - average_assignment_time: calculate_average_time(conversations, :assignment_time), - successful_assignments: conversations.where(success: true).count, - failed_assignments: conversations.where(success: false).count - } - end - - def compute_agent_utilization(agent) - conversations = agent.assigned_conversations.where(created_at: date_range) - capacity_limit = fetch_agent_capacity_limit(agent) - - { - agent_id: agent.id, - agent_name: agent.name, - current_load: agent.assigned_conversations.open.count, - capacity_limit: capacity_limit, - utilization_percentage: calculate_utilization_percentage(agent, capacity_limit), - total_handled: conversations.count, - average_handling_time: calculate_average_handling_time(conversations) - } - end - - def compute_distribution_by_inbox - Conversation.joins(:inbox) - .where(created_at: date_range, account_id: account.id) - .where.not(assignee_id: nil) - .group('inboxes.name') - .count - end - - def compute_distribution_by_team - Conversation.joins(assignee: { team_members: :team }) - .where(created_at: date_range, account_id: account.id) - .group('teams.name') - .count - end - - def compute_distribution_by_hour - Conversation.where(created_at: date_range, account_id: account.id) - .where.not(assignee_id: nil) - .group_by_hour(:created_at, format: '%H') - .count - end - - def compute_period_metrics - group_by = params[:group_by] || 'day' - conversations = filter_conversations_by_date_range - - case group_by - when 'hour' - group_by_hour(conversations) - when 'week' - group_by_week(conversations) - when 'month' - group_by_month(conversations) - else - group_by_day(conversations) - end - end - - private - - def date_range - @date_range ||= params[:since]..params[:until] - end - - def filter_conversations_by_date_range - Conversation.where(account_id: account.id, created_at: date_range) - end - - def total_assigned_conversations - filter_conversations_by_date_range.where.not(assignee_id: nil).count - end - - def calculate_assignment_rate - total = filter_conversations_by_date_range.count - return 0.0 if total.zero? - - (total_assigned_conversations.to_f / total * 100).round(2) - end - - def calculate_average_response_time - # Implementation for average response time - 0 - end - - def calculate_average_resolution_time - # Implementation for average resolution time - 0 - end - - def assignments_by_policy - # Implementation for assignments by policy - {} - end - - def fetch_agent_capacity_limit(_agent) - # Implementation to fetch agent capacity limit - nil - end - - def calculate_utilization_percentage(agent, capacity_limit) - return 0.0 unless capacity_limit&.positive? - - current_load = agent.assigned_conversations.open.count - (current_load.to_f / capacity_limit * 100).round(2) - end - - def calculate_average_handling_time(_conversations) - # Implementation for average handling time - 0 - end - - def calculate_average_time(_conversations, _field) - # Implementation for average time calculation - 0 - end - - def group_by_hour(conversations) - conversations.group_by_hour(:created_at).count - end - - def group_by_day(conversations) - conversations.group_by_day(:created_at).count - end - - def group_by_week(conversations) - conversations.group_by_week(:created_at).count - end - - def group_by_month(conversations) - conversations.group_by_month(:created_at).count - end -end diff --git a/config/routes.rb b/config/routes.rb index 3ebfd0293..81715922c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -244,19 +244,6 @@ Rails.application.routes.draw do # Agent capacity status get 'agents/:agent_id/capacity', to: 'agent_capacity_policies#agent_capacity' - # Assignment Metrics - namespace :reports do - resources :assignment_metrics, only: [:index] do - collection do - get 'agent_history' - get 'policy_performance' - get 'agent_utilization' - get 'assignment_distribution' - get 'export' - end - end - end - namespace :twitter do resource :authorization, only: [:create] end