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" />