add crud for assignment and agent capacity
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::AgentCapacityPoliciesController < Api::V1::Accounts::BaseController
|
||||
before_action :ensure_enterprise_account
|
||||
before_action :fetch_agent_capacity_policy, only: [:show, :update, :destroy, :set_inbox_limit, :remove_inbox_limit, :assign_user, :remove_user]
|
||||
before_action :check_authorization
|
||||
|
||||
def index
|
||||
@agent_capacity_policies = Current.account.agent_capacity_policies.includes(:users, :inbox_capacity_limits)
|
||||
render json: { agent_capacity_policies: serialize_agent_capacity_policies(@agent_capacity_policies) }
|
||||
end
|
||||
|
||||
def show
|
||||
render json: { agent_capacity_policy: serialize_agent_capacity_policy(@agent_capacity_policy) }
|
||||
end
|
||||
|
||||
def create
|
||||
@agent_capacity_policy = Current.account.agent_capacity_policies.build(agent_capacity_policy_params)
|
||||
|
||||
if @agent_capacity_policy.save
|
||||
render json: { agent_capacity_policy: serialize_agent_capacity_policy(@agent_capacity_policy) }, status: :created
|
||||
else
|
||||
render json: { errors: @agent_capacity_policy.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @agent_capacity_policy.update(agent_capacity_policy_params)
|
||||
render json: { agent_capacity_policy: serialize_agent_capacity_policy(@agent_capacity_policy) }
|
||||
else
|
||||
render json: { errors: @agent_capacity_policy.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @agent_capacity_policy.destroy
|
||||
head :ok
|
||||
else
|
||||
render json: { errors: @agent_capacity_policy.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# Set inbox capacity limit for a policy
|
||||
def set_inbox_limit
|
||||
inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
inbox_limit = @agent_capacity_policy.inbox_capacity_limits.find_or_initialize_by(inbox: inbox)
|
||||
|
||||
if inbox_limit.update(conversation_limit: params[:conversation_limit])
|
||||
render json: {
|
||||
inbox_capacity_limit: serialize_inbox_capacity_limit(inbox_limit)
|
||||
}
|
||||
else
|
||||
render json: { errors: inbox_limit.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# Remove inbox capacity limit from a policy
|
||||
def remove_inbox_limit
|
||||
inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
inbox_limit = @agent_capacity_policy.inbox_capacity_limits.find_by(inbox: inbox)
|
||||
|
||||
if inbox_limit
|
||||
if inbox_limit.destroy
|
||||
head :ok
|
||||
else
|
||||
render json: { errors: inbox_limit.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
else
|
||||
render json: { error: 'Inbox limit not found' }, status: :not_found
|
||||
end
|
||||
end
|
||||
|
||||
# Assign a user to a capacity policy
|
||||
def assign_user
|
||||
user = Current.account.users.find(params[:user_id])
|
||||
|
||||
# Remove user from any existing capacity policy
|
||||
Enterprise::AgentCapacityPolicyUser.where(user: user).destroy_all
|
||||
|
||||
# Assign to new policy
|
||||
policy_user = @agent_capacity_policy.agent_capacity_policy_users.build(user: user)
|
||||
|
||||
if policy_user.save
|
||||
render json: {
|
||||
message: 'User assigned successfully',
|
||||
agent_capacity_policy_user: serialize_policy_user(policy_user)
|
||||
}
|
||||
else
|
||||
render json: { errors: policy_user.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# Remove a user from a capacity policy
|
||||
def remove_user
|
||||
user = Current.account.users.find(params[:user_id])
|
||||
policy_user = @agent_capacity_policy.agent_capacity_policy_users.find_by(user: user)
|
||||
|
||||
if policy_user
|
||||
if policy_user.destroy
|
||||
head :ok
|
||||
else
|
||||
render json: { errors: policy_user.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
else
|
||||
render json: { error: 'User not assigned to this policy' }, status: :not_found
|
||||
end
|
||||
end
|
||||
|
||||
# Get current capacity status for an agent
|
||||
def agent_capacity
|
||||
user = Current.account.users.find(params[:agent_id])
|
||||
inbox = params[:inbox_id] ? Current.account.inboxes.find(params[:inbox_id]) : nil
|
||||
|
||||
capacity_service = Enterprise::AssignmentV2::CapacityService.new
|
||||
capacity_data = if inbox
|
||||
capacity_service.get_agent_capacity(user, inbox)
|
||||
else
|
||||
capacity_service.get_agent_overall_capacity(user)
|
||||
end
|
||||
|
||||
render json: { agent_capacity: capacity_data }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_enterprise_account
|
||||
unless Current.account.feature_enabled?(:enterprise_agent_capacity)
|
||||
render json: {
|
||||
error: 'Agent capacity policies are only available for enterprise accounts'
|
||||
}, status: :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_agent_capacity_policy
|
||||
@agent_capacity_policy = Current.account.agent_capacity_policies.find(params[:id])
|
||||
end
|
||||
|
||||
def agent_capacity_policy_params
|
||||
params.require(:agent_capacity_policy).permit(:name, :description, exclusion_rules: {})
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
authorize(Enterprise::AgentCapacityPolicy) if defined?(Enterprise::AgentCapacityPolicy)
|
||||
end
|
||||
|
||||
def serialize_agent_capacity_policy(policy)
|
||||
{
|
||||
id: policy.id,
|
||||
name: policy.name,
|
||||
description: policy.description,
|
||||
exclusion_rules: policy.exclusion_rules,
|
||||
user_count: policy.users.count,
|
||||
inbox_limit_count: policy.inbox_capacity_limits.count,
|
||||
created_at: policy.created_at,
|
||||
updated_at: policy.updated_at
|
||||
}
|
||||
end
|
||||
|
||||
def serialize_agent_capacity_policies(policies)
|
||||
policies.map { |policy| serialize_agent_capacity_policy(policy) }
|
||||
end
|
||||
|
||||
def serialize_inbox_capacity_limit(limit)
|
||||
{
|
||||
id: limit.id,
|
||||
inbox_id: limit.inbox_id,
|
||||
inbox_name: limit.inbox.name,
|
||||
conversation_limit: limit.conversation_limit,
|
||||
created_at: limit.created_at,
|
||||
updated_at: limit.updated_at
|
||||
}
|
||||
end
|
||||
|
||||
def serialize_policy_user(policy_user)
|
||||
{
|
||||
id: policy_user.id,
|
||||
user_id: policy_user.user_id,
|
||||
user_name: policy_user.user.name,
|
||||
user_email: policy_user.user.email,
|
||||
created_at: policy_user.created_at
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,222 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::AssignmentMetricsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :validate_date_range
|
||||
|
||||
def index
|
||||
@metrics = compute_assignment_metrics
|
||||
render json: { assignment_metrics: @metrics }
|
||||
end
|
||||
|
||||
def agent_history
|
||||
@agent = Current.account.users.find(params[:agent_id])
|
||||
@assignment_history = fetch_agent_assignment_history(@agent)
|
||||
|
||||
render json: {
|
||||
agent: serialize_agent(@agent),
|
||||
assignment_history: @assignment_history,
|
||||
meta: pagination_meta
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compute_assignment_metrics
|
||||
metrics = {
|
||||
summary: compute_summary_metrics,
|
||||
by_period: compute_period_metrics,
|
||||
by_inbox: compute_inbox_metrics,
|
||||
by_agent: compute_agent_metrics,
|
||||
by_policy: compute_policy_metrics
|
||||
}
|
||||
|
||||
metrics
|
||||
end
|
||||
|
||||
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_period_metrics
|
||||
group_by = params[:group_by] || 'day'
|
||||
conversations = filter_conversations_by_date_range
|
||||
|
||||
case group_by
|
||||
when 'hour'
|
||||
group_by_hour(conversations)
|
||||
when 'day'
|
||||
group_by_day(conversations)
|
||||
when 'week'
|
||||
group_by_week(conversations)
|
||||
when 'month'
|
||||
group_by_month(conversations)
|
||||
else
|
||||
group_by_day(conversations)
|
||||
end
|
||||
end
|
||||
|
||||
def compute_inbox_metrics
|
||||
inbox_id = params[:inbox_id]
|
||||
base_query = filter_conversations_by_date_range
|
||||
|
||||
if inbox_id.present?
|
||||
base_query = base_query.where(inbox_id: inbox_id)
|
||||
end
|
||||
|
||||
base_query.joins(:inbox)
|
||||
.group('inboxes.id', 'inboxes.name')
|
||||
.count
|
||||
.map { |k, v| { inbox_id: k[0], inbox_name: k[1], assignment_count: v } }
|
||||
end
|
||||
|
||||
def compute_agent_metrics
|
||||
agent_id = params[:agent_id]
|
||||
base_query = filter_conversations_by_date_range.where.not(assignee_id: nil)
|
||||
|
||||
if agent_id.present?
|
||||
base_query = base_query.where(assignee_id: agent_id)
|
||||
end
|
||||
|
||||
base_query.joins(:assignee)
|
||||
.group('users.id', 'users.name', 'users.email')
|
||||
.count
|
||||
.map { |k, v| { agent_id: k[0], agent_name: k[1], agent_email: k[2], assignment_count: v } }
|
||||
.sort_by { |a| -a[:assignment_count] }
|
||||
end
|
||||
|
||||
def compute_policy_metrics
|
||||
# Get metrics grouped by assignment policy
|
||||
policy_metrics = {}
|
||||
|
||||
Current.account.assignment_policies.includes(:inboxes).each do |policy|
|
||||
inbox_ids = policy.inboxes.pluck(:id)
|
||||
next if inbox_ids.empty?
|
||||
|
||||
conversations = filter_conversations_by_date_range.where(inbox_id: inbox_ids)
|
||||
|
||||
policy_metrics[policy.id] = {
|
||||
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),
|
||||
inbox_count: inbox_ids.count
|
||||
}
|
||||
end
|
||||
|
||||
policy_metrics.values
|
||||
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 filter_conversations_by_date_range
|
||||
Current.account.conversations.where(created_at: date_range)
|
||||
end
|
||||
|
||||
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 validate_date_range
|
||||
if params[:start_date].present? && params[:end_date].present?
|
||||
start_date = Date.parse(params[:start_date])
|
||||
end_date = Date.parse(params[:end_date])
|
||||
|
||||
if start_date > end_date
|
||||
render json: { error: 'Start date must be before end date' }, status: :bad_request
|
||||
elsif (end_date - start_date).to_i > 365
|
||||
render json: { error: 'Date range cannot exceed 365 days' }, status: :bad_request
|
||||
end
|
||||
end
|
||||
rescue Date::Error
|
||||
render json: { error: 'Invalid date format' }, status: :bad_request
|
||||
end
|
||||
|
||||
def calculate_average_assignment_time(conversations)
|
||||
assigned_conversations = conversations.where.not(assignee_id: nil)
|
||||
return 0 if assigned_conversations.empty?
|
||||
|
||||
total_time = assigned_conversations.sum do |conv|
|
||||
assignment_time = conv.assignee_last_seen_at || conv.updated_at
|
||||
(assignment_time - conv.created_at).to_i
|
||||
end
|
||||
|
||||
(total_time / assigned_conversations.count / 60).round(2) # Return in minutes
|
||||
end
|
||||
|
||||
def calculate_assignments_per_agent(conversations)
|
||||
assigned_count = conversations.where.not(assignee_id: nil).count
|
||||
agent_count = conversations.where.not(assignee_id: nil).distinct.count(:assignee_id)
|
||||
|
||||
return 0 if agent_count.zero?
|
||||
|
||||
(assigned_count.to_f / agent_count).round(2)
|
||||
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
|
||||
|
||||
def serialize_agent(agent)
|
||||
{
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
email: agent.email,
|
||||
avatar_url: agent.avatar_url
|
||||
}
|
||||
end
|
||||
|
||||
def pagination_meta
|
||||
{
|
||||
current_page: params[:page] || 1,
|
||||
per_page: params[:per_page] || 50
|
||||
}
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
authorize(Conversation, :index?)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,78 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::AssignmentPoliciesController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_assignment_policy, only: [:show, :update, :destroy]
|
||||
before_action :check_authorization
|
||||
|
||||
def index
|
||||
@assignment_policies = Current.account.assignment_policies.includes(:inboxes)
|
||||
render json: { assignment_policies: serialize_assignment_policies(@assignment_policies) }
|
||||
end
|
||||
|
||||
def show
|
||||
render json: { assignment_policy: serialize_assignment_policy(@assignment_policy) }
|
||||
end
|
||||
|
||||
def create
|
||||
@assignment_policy = Current.account.assignment_policies.build(assignment_policy_params)
|
||||
|
||||
if @assignment_policy.save
|
||||
render json: { assignment_policy: serialize_assignment_policy(@assignment_policy) }, status: :created
|
||||
else
|
||||
render json: { errors: @assignment_policy.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @assignment_policy.update(assignment_policy_params)
|
||||
render json: { assignment_policy: serialize_assignment_policy(@assignment_policy) }
|
||||
else
|
||||
render json: { errors: @assignment_policy.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @assignment_policy.destroy
|
||||
head :ok
|
||||
else
|
||||
render json: { errors: @assignment_policy.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_assignment_policy
|
||||
@assignment_policy = Current.account.assignment_policies.find(params[:id])
|
||||
end
|
||||
|
||||
def assignment_policy_params
|
||||
params.require(:assignment_policy).permit(
|
||||
:name, :description, :assignment_order, :conversation_priority,
|
||||
:fair_distribution_limit, :fair_distribution_window, :enabled
|
||||
)
|
||||
end
|
||||
|
||||
def serialize_assignment_policy(policy)
|
||||
{
|
||||
id: policy.id,
|
||||
name: policy.name,
|
||||
description: policy.description,
|
||||
assignment_order: policy.assignment_order,
|
||||
conversation_priority: policy.conversation_priority,
|
||||
fair_distribution_limit: policy.fair_distribution_limit,
|
||||
fair_distribution_window: policy.fair_distribution_window,
|
||||
enabled: policy.enabled,
|
||||
inbox_count: policy.inboxes.count,
|
||||
created_at: policy.created_at,
|
||||
updated_at: policy.updated_at
|
||||
}
|
||||
end
|
||||
|
||||
def serialize_assignment_policies(policies)
|
||||
policies.map { |policy| serialize_assignment_policy(policy) }
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
authorize(AssignmentPolicy)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::InboxAssignmentPoliciesController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_inbox
|
||||
before_action :check_authorization
|
||||
|
||||
def show
|
||||
@inbox_assignment_policy = @inbox.inbox_assignment_policy
|
||||
|
||||
if @inbox_assignment_policy
|
||||
render json: {
|
||||
inbox_assignment_policy: serialize_inbox_assignment_policy(@inbox_assignment_policy)
|
||||
}
|
||||
else
|
||||
render json: {
|
||||
inbox_assignment_policy: nil,
|
||||
message: 'No assignment policy assigned to this inbox'
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
# Remove existing assignment if any
|
||||
@inbox.inbox_assignment_policy&.destroy
|
||||
|
||||
@assignment_policy = Current.account.assignment_policies.find(params[:assignment_policy_id])
|
||||
@inbox_assignment_policy = @inbox.build_inbox_assignment_policy(assignment_policy: @assignment_policy)
|
||||
|
||||
if @inbox_assignment_policy.save
|
||||
render json: {
|
||||
inbox_assignment_policy: serialize_inbox_assignment_policy(@inbox_assignment_policy)
|
||||
}, status: :created
|
||||
else
|
||||
render json: { errors: @inbox_assignment_policy.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@inbox_assignment_policy = @inbox.inbox_assignment_policy
|
||||
|
||||
if @inbox_assignment_policy
|
||||
if @inbox_assignment_policy.destroy
|
||||
head :ok
|
||||
else
|
||||
render json: { errors: @inbox_assignment_policy.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
else
|
||||
render json: { error: 'No assignment policy found for this inbox' }, status: :not_found
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_inbox
|
||||
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
authorize(@inbox, :update?)
|
||||
end
|
||||
|
||||
def serialize_inbox_assignment_policy(inbox_assignment_policy)
|
||||
{
|
||||
id: inbox_assignment_policy.id,
|
||||
inbox_id: inbox_assignment_policy.inbox_id,
|
||||
assignment_policy_id: inbox_assignment_policy.assignment_policy_id,
|
||||
assignment_policy: {
|
||||
id: inbox_assignment_policy.assignment_policy.id,
|
||||
name: inbox_assignment_policy.assignment_policy.name,
|
||||
description: inbox_assignment_policy.assignment_policy.description,
|
||||
assignment_order: inbox_assignment_policy.assignment_policy.assignment_order,
|
||||
conversation_priority: inbox_assignment_policy.assignment_policy.conversation_priority,
|
||||
fair_distribution_limit: inbox_assignment_policy.assignment_policy.fair_distribution_limit,
|
||||
fair_distribution_window: inbox_assignment_policy.assignment_policy.fair_distribution_window,
|
||||
enabled: inbox_assignment_policy.assignment_policy.enabled
|
||||
},
|
||||
created_at: inbox_assignment_policy.created_at,
|
||||
updated_at: inbox_assignment_policy.updated_at
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -102,6 +102,7 @@ class Account < ApplicationRecord
|
||||
|
||||
# Assignment V2 associations
|
||||
has_many :assignment_policies, dependent: :destroy_async
|
||||
has_many :agent_capacity_policies, dependent: :destroy_async, class_name: 'Enterprise::AgentCapacityPolicy' if ChatwootApp.enterprise?
|
||||
|
||||
has_one_attached :contacts_export
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module AssignmentV2FeatureFlag
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def assignment_v2_enabled?
|
||||
# Check account-level feature flag
|
||||
return false unless feature_enabled_for_account?
|
||||
|
||||
# Check for any inbox-level overrides
|
||||
return false if inbox_level_override_disabled?
|
||||
|
||||
# Check system-wide killswitch
|
||||
!GlobalConfig.get('assignment_v2_disabled', false)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def feature_enabled_for_account?
|
||||
config = GlobalConfig.get('assignment_v2')
|
||||
return false unless config&.dig('enabled')
|
||||
|
||||
# If no account allowlist, enable for all
|
||||
allowed_accounts = config.dig('accounts')
|
||||
return true if allowed_accounts.blank?
|
||||
|
||||
# Check if account is in allowlist
|
||||
allowed_accounts.include?(self.id)
|
||||
end
|
||||
|
||||
def inbox_level_override_disabled?
|
||||
return false unless respond_to?(:id)
|
||||
|
||||
# Allow per-inbox disabling during migration
|
||||
GlobalConfig.get("assignment_v2_disabled_inboxes", []).include?(self.id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AssignmentPolicyPolicy < ApplicationPolicy
|
||||
def index?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def show?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def create?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def update?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
@account_user.administrator?
|
||||
end
|
||||
end
|
||||
@@ -224,6 +224,30 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
# Assignment V2 Routes
|
||||
resources :assignment_policies
|
||||
|
||||
resources :inboxes, only: [] do
|
||||
resource :assignment_policy, only: [:show, :create, :destroy], controller: 'inbox_assignment_policies'
|
||||
end
|
||||
|
||||
# Agent Capacity Management (Enterprise)
|
||||
resources :agent_capacity_policies do
|
||||
member do
|
||||
post 'inbox_limits/:inbox_id', to: 'agent_capacity_policies#set_inbox_limit'
|
||||
delete 'inbox_limits/:inbox_id', to: 'agent_capacity_policies#remove_inbox_limit'
|
||||
post 'users', to: 'agent_capacity_policies#assign_user'
|
||||
delete 'users/:user_id', to: 'agent_capacity_policies#remove_user'
|
||||
end
|
||||
end
|
||||
|
||||
# Agent capacity status
|
||||
get 'agents/:agent_id/capacity', to: 'agent_capacity_policies#agent_capacity'
|
||||
|
||||
# Assignment Metrics
|
||||
get 'reports/assignment_metrics', to: 'assignment_metrics#index'
|
||||
get 'agents/:agent_id/assignment_history', to: 'assignment_metrics#agent_history'
|
||||
|
||||
namespace :twitter do
|
||||
resource :authorization, only: [:create]
|
||||
end
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: enterprise_agent_capacity_policy_users
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# agent_capacity_policy_id :bigint not null
|
||||
# user_id :bigint not null
|
||||
# created_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# unique_user_capacity_policy (user_id) UNIQUE
|
||||
# idx_capacity_policy_users_policy_id (agent_capacity_policy_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (agent_capacity_policy_id => enterprise_agent_capacity_policies.id)
|
||||
# fk_rails_... (user_id => users.id)
|
||||
#
|
||||
|
||||
module Enterprise
|
||||
class AgentCapacityPolicyUser < ApplicationRecord
|
||||
self.table_name = 'enterprise_agent_capacity_policy_users'
|
||||
|
||||
# Associations
|
||||
belongs_to :agent_capacity_policy, class_name: 'Enterprise::AgentCapacityPolicy'
|
||||
belongs_to :user
|
||||
|
||||
# Validations
|
||||
validates :user_id, uniqueness: true
|
||||
|
||||
# Delegations
|
||||
delegate :account, to: :agent_capacity_policy
|
||||
|
||||
# Callbacks
|
||||
after_create_commit :invalidate_user_cache
|
||||
after_destroy_commit :invalidate_user_cache
|
||||
|
||||
private
|
||||
|
||||
def invalidate_user_cache
|
||||
Rails.cache.delete_matched("assignment_v2:capacity:#{user_id}:*")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Enterprise
|
||||
class AgentCapacityPolicyPolicy < ApplicationPolicy
|
||||
def index?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def show?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def create?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def update?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def set_inbox_limit?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def remove_inbox_limit?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def assign_user?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def remove_user?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def agent_capacity?
|
||||
@account_user.administrator? || @account_user.agent?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,175 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Enterprise
|
||||
module AssignmentV2
|
||||
class CapacityService
|
||||
def initialize
|
||||
@cache_ttl = 5.minutes
|
||||
end
|
||||
|
||||
# Get agent's current capacity status for specific inbox
|
||||
def get_agent_capacity(agent, inbox)
|
||||
cache_key = capacity_cache_key(agent, inbox)
|
||||
|
||||
cached = Redis::Alfred.hgetall(cache_key)
|
||||
return parse_cached_capacity(cached) if cached.present?
|
||||
|
||||
# Cache miss - compute from database
|
||||
capacity = compute_agent_capacity(agent, inbox)
|
||||
cache_capacity_data(cache_key, capacity)
|
||||
capacity
|
||||
end
|
||||
|
||||
# Get agent's overall capacity across all inboxes
|
||||
def get_agent_overall_capacity(agent)
|
||||
account = agent.accounts.first # Assuming we're working within account context
|
||||
policy = get_agent_capacity_policy(agent, account)
|
||||
|
||||
return unlimited_capacity_summary unless policy
|
||||
|
||||
inboxes_data = []
|
||||
total_current = 0
|
||||
total_limit = 0
|
||||
|
||||
policy.inbox_capacity_limits.includes(:inbox).each do |inbox_limit|
|
||||
inbox = inbox_limit.inbox
|
||||
current = count_current_assignments(agent, inbox, policy)
|
||||
limit = inbox_limit.conversation_limit
|
||||
|
||||
total_current += current
|
||||
total_limit += limit
|
||||
|
||||
inboxes_data << {
|
||||
inbox_id: inbox.id,
|
||||
inbox_name: inbox.name,
|
||||
current_assignments: current,
|
||||
conversation_limit: limit,
|
||||
available_capacity: [limit - current, 0].max
|
||||
}
|
||||
end
|
||||
|
||||
{
|
||||
policy_id: policy.id,
|
||||
policy_name: policy.name,
|
||||
total_current_assignments: total_current,
|
||||
total_conversation_limit: total_limit,
|
||||
total_available_capacity: [total_limit - total_current, 0].max,
|
||||
exclusion_rules: policy.exclusion_rules,
|
||||
inboxes: inboxes_data
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compute_agent_capacity(agent, inbox)
|
||||
account = inbox.account
|
||||
policy = get_agent_capacity_policy(agent, account)
|
||||
|
||||
return unlimited_capacity if policy.nil?
|
||||
|
||||
inbox_limit = policy.inbox_capacity_limits.find_by(inbox: inbox)
|
||||
return unlimited_capacity if inbox_limit.nil?
|
||||
|
||||
current_assignments = count_current_assignments(agent, inbox, policy)
|
||||
|
||||
{
|
||||
total_capacity: inbox_limit.conversation_limit,
|
||||
current_assignments: current_assignments,
|
||||
available_capacity: [inbox_limit.conversation_limit - current_assignments, 0].max,
|
||||
policy_id: policy.id,
|
||||
policy_name: policy.name,
|
||||
exclusion_rules: policy.exclusion_rules
|
||||
}
|
||||
end
|
||||
|
||||
def get_agent_capacity_policy(agent, account)
|
||||
account_user = account.account_users.find_by(user: agent)
|
||||
return nil unless account_user&.agent_capacity_policy_id
|
||||
|
||||
Enterprise::AgentCapacityPolicy.find_by(id: account_user.agent_capacity_policy_id)
|
||||
end
|
||||
|
||||
def count_current_assignments(agent, inbox, policy)
|
||||
scope = agent.assigned_conversations
|
||||
.where(inbox: inbox, status: 'open')
|
||||
|
||||
# Apply exclusion rules from policy
|
||||
scope = apply_exclusion_rules(scope, policy)
|
||||
scope.count
|
||||
end
|
||||
|
||||
def apply_exclusion_rules(scope, policy)
|
||||
rules = policy.exclusion_rules || {}
|
||||
|
||||
# Exclude conversations with specific labels
|
||||
if rules['labels'].present?
|
||||
scope = scope.where.not(id:
|
||||
ConversationLabel.joins(:label)
|
||||
.where(labels: { title: rules['labels'] })
|
||||
.select(:conversation_id)
|
||||
)
|
||||
end
|
||||
|
||||
# Exclude conversations older than X hours
|
||||
if rules['hours_threshold'].present?
|
||||
cutoff = rules['hours_threshold'].hours.ago
|
||||
scope = scope.where('conversations.created_at > ?', cutoff)
|
||||
end
|
||||
|
||||
scope
|
||||
end
|
||||
|
||||
def unlimited_capacity
|
||||
{
|
||||
total_capacity: Float::INFINITY,
|
||||
current_assignments: 0,
|
||||
available_capacity: Float::INFINITY,
|
||||
policy_id: nil,
|
||||
policy_name: 'No capacity policy',
|
||||
exclusion_rules: {}
|
||||
}
|
||||
end
|
||||
|
||||
def unlimited_capacity_summary
|
||||
{
|
||||
policy_id: nil,
|
||||
policy_name: 'No capacity policy',
|
||||
total_current_assignments: 0,
|
||||
total_conversation_limit: Float::INFINITY,
|
||||
total_available_capacity: Float::INFINITY,
|
||||
exclusion_rules: {},
|
||||
inboxes: []
|
||||
}
|
||||
end
|
||||
|
||||
def parse_cached_capacity(cached)
|
||||
{
|
||||
total_capacity: cached['total_capacity'].to_i,
|
||||
current_assignments: cached['current_assignments'].to_i,
|
||||
available_capacity: cached['available_capacity'].to_i,
|
||||
policy_id: cached['policy_id'].presence&.to_i,
|
||||
policy_name: cached['policy_name'] || 'No capacity policy',
|
||||
exclusion_rules: JSON.parse(cached['exclusion_rules'] || '{}')
|
||||
}
|
||||
end
|
||||
|
||||
def cache_capacity_data(cache_key, capacity)
|
||||
Redis::Alfred.multi do |multi|
|
||||
multi.hset(cache_key,
|
||||
'total_capacity', capacity[:total_capacity],
|
||||
'current_assignments', capacity[:current_assignments],
|
||||
'available_capacity', capacity[:available_capacity],
|
||||
'policy_id', capacity[:policy_id],
|
||||
'policy_name', capacity[:policy_name],
|
||||
'exclusion_rules', capacity[:exclusion_rules].to_json
|
||||
)
|
||||
multi.expire(cache_key, @cache_ttl)
|
||||
end
|
||||
end
|
||||
|
||||
def capacity_cache_key(agent, inbox)
|
||||
"assignment_v2:capacity:#{agent.id}:#{inbox.id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user