From 5e8fd689c91f1387011703003408ef7318b6d1e7 Mon Sep 17 00:00:00 2001 From: "Aswin Dev P.S" Date: Tue, 29 Mar 2022 10:31:52 +0530 Subject: [PATCH] feat: Add live agent load report api (#4297) This change allows the admin user to fetch conversation metrics for an account, agents, and filter conversation metrics for a specific agent. Fixes #4305 --- app/builders/v2/report_builder.rb | 81 ++---- .../api/v2/accounts/reports_controller.rb | 17 ++ app/helpers/report_helper.rb | 62 +++++ config/routes.rb | 1 + .../api/v2/accounts/report_controller_spec.rb | 62 +++++ swagger/definitions/index.yml | 15 +- .../resource/reports/conversation/agent.yml | 18 ++ .../{report.yml => reports/summary.yml} | 0 .../reports/conversation/account.yml | 23 ++ .../reports/conversation/agent.yml | 18 ++ swagger/paths/application/reports/index.yml | 7 +- swagger/paths/application/reports/summary.yml | 6 +- swagger/paths/index.yml | 32 +++ swagger/swagger.json | 232 ++++++++++++++---- 14 files changed, 455 insertions(+), 119 deletions(-) create mode 100644 app/helpers/report_helper.rb create mode 100644 swagger/definitions/resource/reports/conversation/agent.yml rename swagger/definitions/resource/{report.yml => reports/summary.yml} (100%) create mode 100644 swagger/paths/application/reports/conversation/account.yml create mode 100644 swagger/paths/application/reports/conversation/agent.yml diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb index b1c09cb63..67b80b7a5 100644 --- a/app/builders/v2/report_builder.rb +++ b/app/builders/v2/report_builder.rb @@ -1,5 +1,6 @@ class V2::ReportBuilder include DateRangeHelper + include ReportHelper attr_reader :account, :params DEFAULT_GROUP_BY = 'day'.freeze @@ -40,23 +41,16 @@ class V2::ReportBuilder } end - private - - def scope - case params[:type] - when :account - account - when :inbox - inbox - when :agent - user - when :label - label - when :team - team + def conversation_metrics + if params[:type].equal?(:account) + conversations + else + agent_metrics end end + private + def inbox @inbox ||= account.inboxes.find(params[:id]) end @@ -84,47 +78,26 @@ class V2::ReportBuilder ) end - def conversations_count - (get_grouped_values scope.conversations).count + def agent_metrics + users = @account.users + users = users.where(id: params[:user_id]) if params[:user_id].present? + users.each_with_object([]) do |user, arr| + @user = user + arr << { + user: { id: user.id, name: user.name, thumbnail: user.avatar_url }, + metric: conversations + } + end end - def incoming_messages_count - (get_grouped_values scope.messages.incoming.unscope(:order)).count - end - - def outgoing_messages_count - (get_grouped_values scope.messages.outgoing.unscope(:order)).count - end - - def resolutions_count - (get_grouped_values scope.conversations.resolved).count - end - - def avg_first_response_time - (get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value) - end - - def avg_resolution_time - (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value) - end - - def avg_resolution_time_summary - avg_rt = scope.reporting_events - .where(name: 'conversation_resolved', created_at: range) - .average(:value) - - return 0 if avg_rt.blank? - - avg_rt - end - - def avg_first_response_time_summary - avg_frt = scope.reporting_events - .where(name: 'first_response', created_at: range) - .average(:value) - - return 0 if avg_frt.blank? - - avg_frt + def conversations + @open_conversations = scope.conversations.open + first_response_count = scope.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count + metric = { + open: @open_conversations.count, + unattended: @open_conversations.count - first_response_count + } + metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account) + metric end end diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index efa09a43c..b2932e34f 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -35,6 +35,12 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv' end + def conversations + return head :unprocessable_entity if params[:type].blank? + + render json: conversation_metrics + end + private def check_authorization @@ -73,6 +79,13 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController } end + def conversation_params + { + type: params[:type].to_sym, + user_id: params[:user_id] + } + end + def range { current: { @@ -91,4 +104,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary summary end + + def conversation_metrics + V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics + end end diff --git a/app/helpers/report_helper.rb b/app/helpers/report_helper.rb new file mode 100644 index 000000000..31e2c1ce8 --- /dev/null +++ b/app/helpers/report_helper.rb @@ -0,0 +1,62 @@ +module ReportHelper + private + + def scope + case params[:type] + when :account + account + when :inbox + inbox + when :agent + user + when :label + label + when :team + team + end + end + + def conversations_count + (get_grouped_values scope.conversations).count + end + + def incoming_messages_count + (get_grouped_values scope.messages.incoming.unscope(:order)).count + end + + def outgoing_messages_count + (get_grouped_values scope.messages.outgoing.unscope(:order)).count + end + + def resolutions_count + (get_grouped_values scope.conversations.resolved).count + end + + def avg_first_response_time + (get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value) + end + + def avg_resolution_time + (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value) + end + + def avg_resolution_time_summary + avg_rt = scope.reporting_events + .where(name: 'conversation_resolved', created_at: range) + .average(:value) + + return 0 if avg_rt.blank? + + avg_rt + end + + def avg_first_response_time_summary + avg_frt = scope.reporting_events + .where(name: 'first_response', created_at: range) + .average(:value) + + return 0 if avg_frt.blank? + + avg_frt + end +end diff --git a/config/routes.rb b/config/routes.rb index 3b6578b8c..a2afa4f09 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -212,6 +212,7 @@ Rails.application.routes.draw do get :inboxes get :labels get :teams + get :conversations end end end diff --git a/spec/controllers/api/v2/accounts/report_controller_spec.rb b/spec/controllers/api/v2/accounts/report_controller_spec.rb index 35febbe25..cb2b9b72d 100644 --- a/spec/controllers/api/v2/accounts/report_controller_spec.rb +++ b/spec/controllers/api/v2/accounts/report_controller_spec.rb @@ -57,6 +57,68 @@ RSpec.describe 'Reports API', type: :request do expect(current_day_metric.length).to eq(1) expect(current_day_metric[0]['value']).to eq(10) end + + it 'return conversation metrics in account level' do + unassigned_conversation = create(:conversation, account: account, inbox: inbox, + assignee: nil, created_at: Time.zone.today) + unassigned_conversation.save! + + get "/api/v2/accounts/#{account.id}/reports/conversations", + params: { + type: :account + }, + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + json_response = JSON.parse(response.body) + + expect(json_response['open']).to eq(11) + expect(json_response['unattended']).to eq(11) + expect(json_response['unassigned']).to eq(1) + end + + it 'return conversation metrics for user in account level' do + create_list(:conversation, 2, account: account, inbox: inbox, + assignee: admin, created_at: Time.zone.today) + + get "/api/v2/accounts/#{account.id}/reports/conversations", + params: { + type: :agent + }, + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + + json_response = JSON.parse(response.body) + expect(json_response.blank?).to be false + user_metrics = json_response.find { |item| item['user']['name'] == admin[:name] } + expect(user_metrics.present?).to be true + + expect(user_metrics['metric']['open']).to eq(2) + expect(user_metrics['metric']['unattended']).to eq(2) + end + + it 'return conversation metrics for specific user in account level' do + create_list(:conversation, 2, account: account, inbox: inbox, + assignee: admin, created_at: Time.zone.today) + + get "/api/v2/accounts/#{account.id}/reports/conversations", + params: { + type: :agent, + user_id: user.id + }, + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + + json_response = JSON.parse(response.body) + expect(json_response.blank?).to be false + expect(json_response[0]['metric']['open']).to eq(10) + expect(json_response[0]['metric']['unattended']).to eq(10) + end end end diff --git a/swagger/definitions/index.yml b/swagger/definitions/index.yml index 7ee10e56d..c120320c8 100644 --- a/swagger/definitions/index.yml +++ b/swagger/definitions/index.yml @@ -149,10 +149,11 @@ extended_message: - $ref: ./resource/extension/message/with_source_sender.yml -## report list -report: - type: array - description: 'array of conversation count based on date' - items: - allOf: - - $ref: './resource/report.yml' +## report +account_summary: + $ref: './resource/reports/summary.yml' +agent_conversation_metrics: + $ref: './resource/reports/conversation/agent.yml' + + + diff --git a/swagger/definitions/resource/reports/conversation/agent.yml b/swagger/definitions/resource/reports/conversation/agent.yml new file mode 100644 index 000000000..7077b767b --- /dev/null +++ b/swagger/definitions/resource/reports/conversation/agent.yml @@ -0,0 +1,18 @@ +type: object +properties: + user: + type: object + properties: + id: + type: number + name: + type: string + thumbnail: + type: string + metric: + type: object + properties: + open: + type: number + unattended: + type: number \ No newline at end of file diff --git a/swagger/definitions/resource/report.yml b/swagger/definitions/resource/reports/summary.yml similarity index 100% rename from swagger/definitions/resource/report.yml rename to swagger/definitions/resource/reports/summary.yml diff --git a/swagger/paths/application/reports/conversation/account.yml b/swagger/paths/application/reports/conversation/account.yml new file mode 100644 index 000000000..28aac72e7 --- /dev/null +++ b/swagger/paths/application/reports/conversation/account.yml @@ -0,0 +1,23 @@ +tags: + - Reports +operationId: get-account-conversation-metrics +summary: Account Conversation Metrics +description: Get conversation metrics for Account +responses: + 200: + description: Success + schema: + type: object + description: 'Object of account conversation metrics' + properties: + open: + type: number + unattended: + type: number + unassigned: + type: number + + 404: + description: reports not found + 403: + description: Access denied diff --git a/swagger/paths/application/reports/conversation/agent.yml b/swagger/paths/application/reports/conversation/agent.yml new file mode 100644 index 000000000..90f433e4a --- /dev/null +++ b/swagger/paths/application/reports/conversation/agent.yml @@ -0,0 +1,18 @@ +tags: + - Reports +operationId: get-agent-conversation-metrics +summary: Agent Conversation Metrics +description: Get conversation metrics for Agent +responses: + 200: + description: Success + schema: + type: array + description: 'Array of agent based conversation metrics' + items: + $ref: '#/definitions/agent_conversation_metrics' + + 404: + description: reports not found + 403: + description: Access denied diff --git a/swagger/paths/application/reports/index.yml b/swagger/paths/application/reports/index.yml index dacf77a13..3d86df38d 100644 --- a/swagger/paths/application/reports/index.yml +++ b/swagger/paths/application/reports/index.yml @@ -10,7 +10,12 @@ responses: type: array description: 'Array of date based conversation statistics' items: - $ref: '#/definitions/report' + type: object + properties: + value: + type: string + timestamp: + type: number 404: description: reports not found 403: diff --git a/swagger/paths/application/reports/summary.yml b/swagger/paths/application/reports/summary.yml index ec659e8e8..f9538a9f8 100644 --- a/swagger/paths/application/reports/summary.yml +++ b/swagger/paths/application/reports/summary.yml @@ -7,10 +7,8 @@ responses: 200: description: Success schema: - type: array - description: 'Array of date based conversation statistics' - items: - $ref: '#/definitions/report' + description: 'Object of summary metrics' + $ref: '#/definitions/account_summary' 404: description: reports not found 403: diff --git a/swagger/paths/index.yml b/swagger/paths/index.yml index ec2fcc38b..eeb2539f1 100644 --- a/swagger/paths/index.yml +++ b/swagger/paths/index.yml @@ -391,3 +391,35 @@ description: The timestamp from where report should stop. get: $ref: './application/reports/summary.yml' + +# Conversation metrics for account +/api/v2/accounts/{account_id}/reports/conversations: + parameters: + - $ref: '#/parameters/account_id' + - in: query + name: type + type: string + enum: + - account + required: true + description: Type of report + get: + $ref: './application/reports/conversation/account.yml' + +# Conversation metrics for agent +/api/v2/accounts/{account_id}/reports/conversations/: + parameters: + - $ref: '#/parameters/account_id' + - in: query + name: type + type: string + enum: + - agent + required: true + description: Type of report + - in: query + name: user_id + type: string + description: The numeric ID of the user + get: + $ref: './application/reports/conversation/agent.yml' \ No newline at end of file diff --git a/swagger/swagger.json b/swagger/swagger.json index 1fdcd4785..b17f9c586 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -3604,7 +3604,15 @@ "type": "array", "description": "Array of date based conversation statistics", "items": { - "$ref": "#/definitions/report" + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "timestamp": { + "type": "number" + } + } } } }, @@ -3651,14 +3659,110 @@ "operationId": "list-all-conversation-statistics-summary", "summary": "Get Account reports summary", "description": "Get Account reports summary for a specific type and date range", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/account_summary" + } + }, + "404": { + "description": "reports not found" + }, + "403": { + "description": "Access denied" + } + } + } + }, + "/api/v2/accounts/{account_id}/reports/conversations": { + "parameters": [ + { + "$ref": "#/parameters/account_id" + }, + { + "in": "query", + "name": "type", + "type": "string", + "enum": [ + "account" + ], + "required": true, + "description": "Type of report" + } + ], + "get": { + "tags": [ + "Reports" + ], + "operationId": "get-account-conversation-metrics", + "summary": "Account Conversation Metrics", + "description": "Get conversation metrics for Account", + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "object", + "description": "Object of account conversation metrics", + "properties": { + "open": { + "type": "number" + }, + "unattended": { + "type": "number" + }, + "unassigned": { + "type": "number" + } + } + } + }, + "404": { + "description": "reports not found" + }, + "403": { + "description": "Access denied" + } + } + } + }, + "/api/v2/accounts/{account_id}/reports/conversations/": { + "parameters": [ + { + "$ref": "#/parameters/account_id" + }, + { + "in": "query", + "name": "type", + "type": "string", + "enum": [ + "agent" + ], + "required": true, + "description": "Type of report" + }, + { + "in": "query", + "name": "user_id", + "type": "string", + "description": "The numeric ID of the user" + } + ], + "get": { + "tags": [ + "Reports" + ], + "operationId": "get-agent-conversation-metrics", + "summary": "Agent Conversation Metrics", + "description": "Get conversation metrics for Agent", "responses": { "200": { "description": "Success", "schema": { "type": "array", - "description": "Array of date based conversation statistics", + "description": "Array of agent based conversation metrics", "items": { - "$ref": "#/definitions/report" + "$ref": "#/definitions/agent_conversation_metrics" } } }, @@ -4879,58 +4983,80 @@ } ] }, - "report": { - "type": "array", - "description": "array of conversation count based on date", - "items": { - "allOf": [ - { - "type": "object", - "properties": { - "avg_first_response_time": { - "type": "string" - }, - "avg_resolution_time": { - "type": "string" - }, - "conversations_count": { - "type": "number" - }, - "incoming_messages_count": { - "type": "number" - }, - "outgoing_messages_count": { - "type": "number" - }, - "resolutions_count": { - "type": "number" - }, - "previous": { - "type": "object", - "properties": { - "avg_first_response_time": { - "type": "string" - }, - "avg_resolution_time": { - "type": "string" - }, - "conversations_count": { - "type": "number" - }, - "incoming_messages_count": { - "type": "number" - }, - "outgoing_messages_count": { - "type": "number" - }, - "resolutions_count": { - "type": "number" - } - } - } + "account_summary": { + "type": "object", + "properties": { + "avg_first_response_time": { + "type": "string" + }, + "avg_resolution_time": { + "type": "string" + }, + "conversations_count": { + "type": "number" + }, + "incoming_messages_count": { + "type": "number" + }, + "outgoing_messages_count": { + "type": "number" + }, + "resolutions_count": { + "type": "number" + }, + "previous": { + "type": "object", + "properties": { + "avg_first_response_time": { + "type": "string" + }, + "avg_resolution_time": { + "type": "string" + }, + "conversations_count": { + "type": "number" + }, + "incoming_messages_count": { + "type": "number" + }, + "outgoing_messages_count": { + "type": "number" + }, + "resolutions_count": { + "type": "number" } } - ] + } + } + }, + "agent_conversation_metrics": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + }, + "thumbnail": { + "type": "string" + } + } + }, + "metric": { + "type": "object", + "properties": { + "open": { + "type": "number" + }, + "unattended": { + "type": "number" + } + } + } } } },