From d5536d65f75be6ebb3238f133df61b88d65c8668 Mon Sep 17 00:00:00 2001 From: "Aswin Dev P.S" Date: Fri, 8 Apr 2022 12:48:18 +0530 Subject: [PATCH] feat: Consider business hours while generating the reports (#4330) * feat: Consider business hours while generating the reports --- Gemfile | 3 ++ Gemfile.lock | 4 ++ .../api/v2/accounts/reports_controller.rb | 40 +++++++-------- app/helpers/report_helper.rb | 22 +++++--- app/helpers/reporting_event_helper.rb | 50 +++++++++++++++++++ app/javascript/dashboard/api/reports.js | 14 +++++- .../assets/scss/widgets/_report.scss | 5 ++ .../assets/scss/widgets/_reports.scss | 18 +++++++ .../dashboard/i18n/locale/en/report.json | 3 +- .../dashboard/settings/reports/Index.vue | 12 ++++- .../reports/components/FilterSelector.vue | 10 ++++ .../reports/components/ReportFilters.vue | 10 ++++ .../reports/components/WootReports.vue | 12 ++++- .../dashboard/store/modules/reports.js | 6 ++- app/listeners/reporting_event_listener.rb | 13 ++++- app/models/reporting_event.rb | 21 ++++---- ...ue_in_business_hours_to_reporting_event.rb | 9 ++++ db/schema.rb | 3 ++ .../reporting_event_listener_spec.rb | 34 +++++++++++++ 19 files changed, 241 insertions(+), 48 deletions(-) create mode 100644 app/helpers/reporting_event_helper.rb create mode 100644 db/migrate/20220329131401_add_value_in_business_hours_to_reporting_event.rb diff --git a/Gemfile b/Gemfile index 297a88497..e94303f45 100644 --- a/Gemfile +++ b/Gemfile @@ -125,6 +125,9 @@ gem 'procore-sift' gem 'email_reply_trimmer' gem 'html2text' +# to calculate working hours +gem 'working_hours' + group :production, :staging do # we dont want request timing out in development while using byebug gem 'rack-timeout' diff --git a/Gemfile.lock b/Gemfile.lock index 6c314817d..4b128f424 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -636,6 +636,9 @@ GEM websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) wisper (2.0.0) + working_hours (1.4.1) + activesupport (>= 3.2) + tzinfo zeitwerk (2.5.4) PLATFORMS @@ -746,6 +749,7 @@ DEPENDENCIES webpacker (~> 5.x) webpush wisper (= 2.0.0) + working_hours RUBY VERSION ruby 3.0.2p107 diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index b2932e34f..227ba2500 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -47,36 +47,36 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController raise Pundit::NotAuthorizedError unless Current.account_user.administrator? end - def current_summary_params + def common_params { type: params[:type].to_sym, id: params[:id], - since: range[:current][:since], - until: range[:current][:until], - group_by: params[:group_by] + group_by: params[:group_by], + business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours]) } end + def current_summary_params + common_params.merge({ + since: range[:current][:since], + until: range[:current][:until] + }) + end + def previous_summary_params - { - type: params[:type].to_sym, - id: params[:id], - since: range[:previous][:since], - until: range[:previous][:until], - group_by: params[:group_by] - } + common_params.merge({ + since: range[:previous][:since], + until: range[:previous][:until] + }) end def report_params - { - metric: params[:metric], - type: params[:type].to_sym, - since: params[:since], - until: params[:until], - id: params[:id], - group_by: params[:group_by], - timezone_offset: params[:timezone_offset] - } + common_params.merge({ + metric: params[:metric], + since: params[:since], + until: params[:until], + timezone_offset: params[:timezone_offset] + }) end def conversation_params diff --git a/app/helpers/report_helper.rb b/app/helpers/report_helper.rb index 31e2c1ce8..9f37295cb 100644 --- a/app/helpers/report_helper.rb +++ b/app/helpers/report_helper.rb @@ -33,17 +33,23 @@ module ReportHelper end def avg_first_response_time - (get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value) + grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'first_response')) + return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours] + + grouped_reporting_events.average(:value) end def avg_resolution_time - (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value) + grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')) + return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours] + + grouped_reporting_events.average(:value) end def avg_resolution_time_summary - avg_rt = scope.reporting_events - .where(name: 'conversation_resolved', created_at: range) - .average(:value) + reporting_events = scope.reporting_events + .where(name: 'conversation_resolved', created_at: range) + avg_rt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value) return 0 if avg_rt.blank? @@ -51,9 +57,9 @@ module ReportHelper end def avg_first_response_time_summary - avg_frt = scope.reporting_events - .where(name: 'first_response', created_at: range) - .average(:value) + reporting_events = scope.reporting_events + .where(name: 'first_response', created_at: range) + avg_frt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value) return 0 if avg_frt.blank? diff --git a/app/helpers/reporting_event_helper.rb b/app/helpers/reporting_event_helper.rb new file mode 100644 index 000000000..eee1af283 --- /dev/null +++ b/app/helpers/reporting_event_helper.rb @@ -0,0 +1,50 @@ +module ReportingEventHelper + def business_hours(inbox, from, to) + return 0 unless inbox.working_hours_enabled? + + inbox_working_hours = configure_working_hours(inbox.working_hours) + return 0 if inbox_working_hours.blank? + + # Configure working hours + WorkingHours::Config.working_hours = inbox_working_hours + + # Configure timezone + WorkingHours::Config.time_zone = inbox.timezone + + # Use inbox timezone to change from & to values. + from_in_inbox_timezone = from.in_time_zone(inbox.timezone).to_time + to_in_inbox_timezone = to.in_time_zone(inbox.timezone).to_time + from_in_inbox_timezone.working_time_until(to_in_inbox_timezone) + end + + private + + def configure_working_hours(working_hours) + working_hours.each_with_object({}) do |working_hour, object| + object[day(working_hour.day_of_week)] = working_hour_range(working_hour) unless working_hour.closed_all_day? + end + end + + def day(day_of_week) + week_days = { + 0 => :sun, + 1 => :mon, + 2 => :tue, + 3 => :wed, + 4 => :thu, + 5 => :fri, + 6 => :sat + } + week_days[day_of_week] + end + + def working_hour_range(working_hour) + { format_time(working_hour.open_hour, working_hour.open_minutes) => format_time(working_hour.close_hour, working_hour.close_minutes) } + end + + def format_time(hour, minute) + hour = hour < 10 ? "0#{hour}" : hour + minute = minute < 10 ? "0#{minute}" : minute + "#{hour}:#{minute}" + end +end diff --git a/app/javascript/dashboard/api/reports.js b/app/javascript/dashboard/api/reports.js index 90f8b34ea..5c39ddbe6 100644 --- a/app/javascript/dashboard/api/reports.js +++ b/app/javascript/dashboard/api/reports.js @@ -8,7 +8,15 @@ class ReportsAPI extends ApiClient { super('reports', { accountScoped: true, apiVersion: 'v2' }); } - getReports(metric, since, until, type = 'account', id, group_by) { + getReports( + metric, + since, + until, + type = 'account', + id, + group_by, + business_hours + ) { return axios.get(`${this.url}`, { params: { metric, @@ -17,12 +25,13 @@ class ReportsAPI extends ApiClient { type, id, group_by, + business_hours, timezone_offset: getTimeOffset(), }, }); } - getSummary(since, until, type = 'account', id, group_by) { + getSummary(since, until, type = 'account', id, group_by, business_hours) { return axios.get(`${this.url}/summary`, { params: { since, @@ -30,6 +39,7 @@ class ReportsAPI extends ApiClient { type, id, group_by, + business_hours, }, }); } diff --git a/app/javascript/dashboard/assets/scss/widgets/_report.scss b/app/javascript/dashboard/assets/scss/widgets/_report.scss index 444dccb7c..5299f1155 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_report.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_report.scss @@ -78,5 +78,10 @@ font-size: $font-size-default; color: $color-gray; } + + .business-hours { + margin: $space-normal; + text-align: center; + } } } diff --git a/app/javascript/dashboard/assets/scss/widgets/_reports.scss b/app/javascript/dashboard/assets/scss/widgets/_reports.scss index bbd03ce9f..742129655 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_reports.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_reports.scss @@ -25,3 +25,21 @@ align-items: center; display: flex; } + +.business-hours { + align-items: center; + display: flex; + justify-content: end; + margin-bottom: var(--space-normal); + margin-left: auto; + padding-right: var(--space-normal); +} + +.business-hours-text { + font-size: var(--font-size-small); +} + +.switch { + margin-bottom: var(--space-zero); + margin-left: var(--space-small); +} diff --git a/app/javascript/dashboard/i18n/locale/en/report.json b/app/javascript/dashboard/i18n/locale/en/report.json index ed730d798..a14704007 100644 --- a/app/javascript/dashboard/i18n/locale/en/report.json +++ b/app/javascript/dashboard/i18n/locale/en/report.json @@ -78,7 +78,8 @@ { "id": 2, "groupBy": "Week" }, { "id": 3, "groupBy": "Month" }, { "id": 4, "groupBy": "Year" } - ] + ], + "BUSINESS_HOURS": "Business Hours" }, "AGENT_REPORTS": { "HEADER": "Agents Overview", diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue index 1b46c474a..f9a9256f4 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue @@ -15,6 +15,7 @@ :filter-items-list="filterItemsList" @date-range-change="onDateRangeChange" @filter-change="onFilterChange" + @business-hours-toggle="onBusinessHoursToggle" />
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue index 885b76283..87a422be0 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue @@ -61,6 +61,12 @@ @input="handleAgentsFilterSelection" />
+
+ {{ $t('REPORT.BUSINESS_HOURS') }} + + + +
diff --git a/app/javascript/dashboard/store/modules/reports.js b/app/javascript/dashboard/store/modules/reports.js index cb1efe3ff..67f468add 100644 --- a/app/javascript/dashboard/store/modules/reports.js +++ b/app/javascript/dashboard/store/modules/reports.js @@ -42,7 +42,8 @@ export const actions = { reportObj.to, reportObj.type, reportObj.id, - reportObj.groupBy + reportObj.groupBy, + reportObj.businessHours ).then(accountReport => { let { data } = accountReport; data = data.filter( @@ -69,7 +70,8 @@ export const actions = { reportObj.to, reportObj.type, reportObj.id, - reportObj.groupBy + reportObj.groupBy, + reportObj.businessHours ) .then(accountSummary => { commit(types.default.SET_ACCOUNT_SUMMARY, accountSummary.data); diff --git a/app/listeners/reporting_event_listener.rb b/app/listeners/reporting_event_listener.rb index 05aa1db4b..d87b6574b 100644 --- a/app/listeners/reporting_event_listener.rb +++ b/app/listeners/reporting_event_listener.rb @@ -1,4 +1,5 @@ class ReportingEventListener < BaseListener + include ReportingEventHelper def conversation_resolved(event) conversation = extract_conversation_and_account(event)[0] time_to_resolve = conversation.updated_at.to_i - conversation.created_at.to_i @@ -6,10 +7,14 @@ class ReportingEventListener < BaseListener reporting_event = ReportingEvent.new( name: 'conversation_resolved', value: time_to_resolve, + value_in_business_hours: business_hours(conversation.inbox, conversation.created_at, + conversation.updated_at), account_id: conversation.account_id, inbox_id: conversation.inbox_id, user_id: conversation.assignee_id, - conversation_id: conversation.id + conversation_id: conversation.id, + event_start_time: conversation.created_at, + event_end_time: conversation.updated_at ) reporting_event.save end @@ -22,10 +27,14 @@ class ReportingEventListener < BaseListener reporting_event = ReportingEvent.new( name: 'first_response', value: first_response_time, + value_in_business_hours: business_hours(conversation.inbox, conversation.created_at, + message.created_at), account_id: conversation.account_id, inbox_id: conversation.inbox_id, user_id: conversation.assignee_id, - conversation_id: conversation.id + conversation_id: conversation.id, + event_start_time: conversation.created_at, + event_end_time: message.created_at ) reporting_event.save end diff --git a/app/models/reporting_event.rb b/app/models/reporting_event.rb index 288b15504..02f6e4d20 100644 --- a/app/models/reporting_event.rb +++ b/app/models/reporting_event.rb @@ -2,15 +2,18 @@ # # Table name: reporting_events # -# id :bigint not null, primary key -# name :string -# value :float -# created_at :datetime not null -# updated_at :datetime not null -# account_id :integer -# conversation_id :integer -# inbox_id :integer -# user_id :integer +# id :bigint not null, primary key +# event_end_time :datetime +# event_start_time :datetime +# name :string +# value :float +# value_in_business_hours :float +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer +# conversation_id :integer +# inbox_id :integer +# user_id :integer # # Indexes # diff --git a/db/migrate/20220329131401_add_value_in_business_hours_to_reporting_event.rb b/db/migrate/20220329131401_add_value_in_business_hours_to_reporting_event.rb new file mode 100644 index 000000000..4b8138eb7 --- /dev/null +++ b/db/migrate/20220329131401_add_value_in_business_hours_to_reporting_event.rb @@ -0,0 +1,9 @@ +class AddValueInBusinessHoursToReportingEvent < ActiveRecord::Migration[6.1] + def change + change_table :reporting_events, bulk: true do |t| + t.float :value_in_business_hours, default: nil + t.datetime :event_start_time, default: nil + t.datetime :event_end_time, default: nil + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c99c2b70c..84619b696 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -657,6 +657,9 @@ ActiveRecord::Schema.define(version: 2022_04_05_092033) do t.integer "conversation_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.float "value_in_business_hours" + t.datetime "event_start_time" + t.datetime "event_end_time" t.index ["account_id"], name: "index_reporting_events_on_account_id" t.index ["created_at"], name: "index_reporting_events_on_created_at" t.index ["inbox_id"], name: "index_reporting_events_on_inbox_id" diff --git a/spec/listeners/reporting_event_listener_spec.rb b/spec/listeners/reporting_event_listener_spec.rb index 399815014..d3b6bd35d 100644 --- a/spec/listeners/reporting_event_listener_spec.rb +++ b/spec/listeners/reporting_event_listener_spec.rb @@ -17,6 +17,21 @@ describe ReportingEventListener do listener.conversation_resolved(event) expect(account.reporting_events.where(name: 'conversation_resolved').count).to be 1 end + + context 'when business hours enabled for inbox' do + let(:created_at) { Time.zone.parse('March 20, 2022 00:00') } + let(:updated_at) { Time.zone.parse('March 26, 2022 23:59') } + let!(:new_inbox) { create(:inbox, working_hours_enabled: true, account: account) } + let!(:new_conversation) do + create(:conversation, created_at: created_at, updated_at: updated_at, account: account, inbox: new_inbox, assignee: user) + end + + it 'creates conversation_resolved event with business hour value' do + event = Events::Base.new('conversation.resolved', Time.zone.now, conversation: new_conversation) + listener.conversation_resolved(event) + expect(account.reporting_events.where(name: 'conversation_resolved')[0]['value_in_business_hours']).to be 144_000.0 + end + end end describe '#first_reply_created' do @@ -26,5 +41,24 @@ describe ReportingEventListener do listener.first_reply_created(event) expect(account.reporting_events.where(name: 'first_response').count).to eql previous_count + 1 end + + context 'when business hours enabled for inbox' do + let(:conversation_created_at) { Time.zone.parse('March 20, 2022 00:00') } + let(:message_created_at) { Time.zone.parse('March 26, 2022 23:59') } + let!(:new_inbox) { create(:inbox, working_hours_enabled: true, account: account) } + let!(:new_conversation) do + create(:conversation, created_at: conversation_created_at, account: account, inbox: new_inbox, assignee: user) + end + let!(:new_message) do + create(:message, message_type: 'outgoing', created_at: message_created_at, + account: account, inbox: new_inbox, conversation: new_conversation) + end + + it 'creates first_response event with business hour value' do + event = Events::Base.new('first.reply.created', Time.zone.now, message: new_message) + listener.first_reply_created(event) + expect(account.reporting_events.where(name: 'first_response')[0]['value_in_business_hours']).to be 144_000.0 + end + end end end