diff --git a/Gemfile b/Gemfile index ffde3d3e3..bbc7a6f74 100644 --- a/Gemfile +++ b/Gemfile @@ -108,9 +108,9 @@ gem 'elastic-apm', require: false gem 'newrelic_rpm', require: false gem 'newrelic-sidekiq-metrics', require: false gem 'scout_apm', require: false -gem 'sentry-rails', require: false +gem 'sentry-rails', '>= 5.10.0', require: false gem 'sentry-ruby', require: false -gem 'sentry-sidekiq', require: false +gem 'sentry-sidekiq', '>= 5.10.0', require: false ##-- background job processing --## gem 'sidekiq' @@ -213,6 +213,7 @@ group :development, :test do gem 'bundle-audit', require: false gem 'byebug', platform: :mri gem 'climate_control' + gem 'debug', '~> 1.8' gem 'factory_bot_rails' gem 'listen' gem 'mock_redis' diff --git a/Gemfile.lock b/Gemfile.lock index 6b52c1e67..0b250bd77 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -164,7 +164,7 @@ GEM coderay (1.1.3) commonmarker (0.23.9) concurrent-ruby (1.2.2) - connection_pool (2.4.0) + connection_pool (2.4.1) crack (0.4.5) rexml crass (1.0.6) @@ -184,6 +184,9 @@ GEM libddwaf (~> 1.8.2.0.0) msgpack debase-ruby_core_source (3.2.0) + debug (1.8.0) + irb (>= 1.5.0) + reline (>= 0.3.1) declarative (0.0.20) devise (4.9.2) bcrypt (~> 3.0) @@ -366,6 +369,9 @@ GEM blingfire (>= 0.1.7) numo-narray onnxruntime (>= 0.5.1) + io-console (0.6.0) + irb (1.7.2) + reline (>= 0.3.6) jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) @@ -470,14 +476,14 @@ GEM sidekiq newrelic_rpm (8.16.0) nio4r (2.5.9) - nokogiri (1.15.2) + nokogiri (1.15.3) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.15.2-arm64-darwin) + nokogiri (1.15.3-arm64-darwin) racc (~> 1.4) - nokogiri (1.15.2-x86_64-darwin) + nokogiri (1.15.3-x86_64-darwin) racc (~> 1.4) - nokogiri (1.15.2-x86_64-linux) + nokogiri (1.15.3-x86_64-linux) racc (~> 1.4) numo-narray (0.9.2.1) oauth (1.1.0) @@ -568,8 +574,9 @@ GEM activesupport (= 7.0.5.1) bundler (>= 1.15.0) railties (= 7.0.5.1) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + rails-dom-testing (2.1.1) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) rails-html-sanitizer (1.6.0) loofah (~> 2.21) @@ -593,6 +600,8 @@ GEM redis-namespace (1.10.0) redis (>= 4) regexp_parser (2.8.0) + reline (0.3.6) + io-console (~> 0.5) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) @@ -683,18 +692,18 @@ GEM activesupport (>= 4) selectize-rails (0.12.6) semantic_range (3.0.0) - sentry-rails (5.9.0) + sentry-rails (5.10.0) railties (>= 5.0) - sentry-ruby (~> 5.9.0) - sentry-ruby (5.9.0) + sentry-ruby (~> 5.10.0) + sentry-ruby (5.10.0) concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.9.0) - sentry-ruby (~> 5.9.0) + sentry-sidekiq (5.10.0) + sentry-ruby (~> 5.10.0) sidekiq (>= 3.0) sexp_processor (4.17.0) shoulda-matchers (5.3.0) activesupport (>= 5.2.0) - sidekiq (7.1.0) + sidekiq (7.1.2) concurrent-ruby (< 2) connection_pool (>= 2.3.0) rack (>= 2.2.4) @@ -835,6 +844,7 @@ DEPENDENCIES cypress-on-rails database_cleaner ddtrace + debug (~> 1.8) devise devise-secure_password! devise_token_auth @@ -904,9 +914,9 @@ DEPENDENCIES scout_apm scss_lint seed_dump - sentry-rails + sentry-rails (>= 5.10.0) sentry-ruby - sentry-sidekiq + sentry-sidekiq (>= 5.10.0) shoulda-matchers sidekiq sidekiq-cron diff --git a/Makefile b/Makefile index 499d0ab6b..16eb80718 100644 --- a/Makefile +++ b/Makefile @@ -30,9 +30,23 @@ burn: bundle && yarn run: + @if [ -f ./.overmind.sock ]; then \ + echo "Overmind is already running. Use 'make force_run' to start a new instance."; \ + else \ + overmind start -f Procfile.dev; \ + fi + +force_run: + rm -f ./.overmind.sock overmind start -f Procfile.dev +debug: + overmind connect backend + +debug_worker: + overmind connect worker + docker: docker build -t $(APP_NAME) -f ./docker/Dockerfile . -.PHONY: setup db_create db_migrate db_seed db console server burn docker run +.PHONY: setup db_create db_migrate db_seed db console server burn docker run force_run debug debug_worker diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb index b5fb8045b..b0f6ba25e 100644 --- a/app/builders/v2/report_builder.rb +++ b/app/builders/v2/report_builder.rb @@ -33,18 +33,18 @@ class V2::ReportBuilder def summary { - conversations_count: conversations_count.values.sum, - incoming_messages_count: incoming_messages_count.values.sum, - outgoing_messages_count: outgoing_messages_count.values.sum, + conversations_count: conversations.count, + incoming_messages_count: incoming_messages.count, + outgoing_messages_count: outgoing_messages.count, avg_first_response_time: avg_first_response_time_summary, avg_resolution_time: avg_resolution_time_summary, - resolutions_count: resolutions_count.values.sum + resolutions_count: resolutions.count } end def conversation_metrics if params[:type].equal?(:account) - conversations + live_conversations else agent_metrics.sort_by { |hash| hash[:metric][:open] }.reverse end @@ -89,12 +89,12 @@ class V2::ReportBuilder email: @user.email, thumbnail: @user.avatar_url, availability: account_user.availability_status, - metric: conversations + metric: live_conversations } end end - def conversations + def live_conversations @open_conversations = scope.conversations.where(account_id: @account.id).open metric = { open: @open_conversations.count, diff --git a/app/helpers/report_helper.rb b/app/helpers/report_helper.rb index 8ee747eb9..796986e5b 100644 --- a/app/helpers/report_helper.rb +++ b/app/helpers/report_helper.rb @@ -17,21 +17,36 @@ module ReportHelper end def conversations_count - (get_grouped_values scope.conversations.where(account_id: account.id)).count + (get_grouped_values conversations).count end def incoming_messages_count - (get_grouped_values scope.messages.where(account_id: account.id).incoming.unscope(:order)).count + (get_grouped_values incoming_messages).count end def outgoing_messages_count - (get_grouped_values scope.messages.where(account_id: account.id).outgoing.unscope(:order)).count + (get_grouped_values outgoing_messages).count end def resolutions_count - object_scope = scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_resolved, - conversations: { status: :resolved }).distinct - (get_grouped_values object_scope).count + (get_grouped_values resolutions).count + end + + def conversations + scope.conversations.where(account_id: account.id, created_at: range) + end + + def incoming_messages + scope.messages.where(account_id: account.id, created_at: range).incoming.unscope(:order) + end + + def outgoing_messages + scope.messages.where(account_id: account.id, created_at: range).outgoing.unscope(:order) + end + + def resolutions + scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_resolved, + conversations: { status: :resolved }, created_at: range).distinct end def avg_first_response_time @@ -51,7 +66,11 @@ module ReportHelper def avg_resolution_time_summary reporting_events = scope.reporting_events .where(name: 'conversation_resolved', account_id: account.id, created_at: range) - avg_rt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value) + avg_rt = if params[:business_hours].present? + reporting_events.average(:value_in_business_hours) + else + reporting_events.average(:value) + end return 0 if avg_rt.blank? @@ -61,7 +80,11 @@ module ReportHelper def avg_first_response_time_summary reporting_events = scope.reporting_events .where(name: 'first_response', account_id: account.id, created_at: range) - avg_frt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value) + avg_frt = if params[:business_hours].present? + reporting_events.average(:value_in_business_hours) + else + reporting_events.average(:value) + end return 0 if avg_frt.blank? diff --git a/app/javascript/dashboard/assets/scss/widgets/_report.scss b/app/javascript/dashboard/assets/scss/widgets/_report.scss index e9ce5d0bd..d07f1dd3d 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_report.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_report.scss @@ -58,31 +58,3 @@ text-transform: capitalize; } } - -.report-bar { - @include background-white; - @include border-light; - margin: var(--space-minus-micro) 0; - padding: var(--space-small) var(--space-medium); - - .chart-container { - @include flex; - @include flex-align(center, middle); - flex-direction: column; - - div { - width: 100%; - } - - .empty-state { - color: $color-gray; - font-size: var(--font-size-default); - margin: var(--space-jumbo); - } - - .business-hours { - margin: var(--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 c848bc0c8..dcd65366a 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_reports.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_reports.scss @@ -22,18 +22,6 @@ margin: 0 var(--space-small); } -.business-hours { - align-items: center; - display: flex; - justify-content: flex-start; - margin-left: auto; - padding-right: var(--space-normal); -} - -.business-hours-text { - font-size: var(--font-size-small); - margin: 0 var(--space-small); -} .switch { margin-bottom: var(--space-zero); diff --git a/app/javascript/dashboard/components/widgets/chart/BarChart.js b/app/javascript/dashboard/components/widgets/chart/BarChart.js index a4dca263b..22ee0f837 100644 --- a/app/javascript/dashboard/components/widgets/chart/BarChart.js +++ b/app/javascript/dashboard/components/widgets/chart/BarChart.js @@ -1,16 +1,20 @@ import { Bar } from 'vue-chartjs'; const fontFamily = - '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; + 'PlusJakarta,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; const defaultChartOptions = { responsive: true, maintainAspectRatio: false, legend: { + display: false, labels: { fontFamily, }, }, + animation: { + duration: 0, + }, datasets: { bar: { barPercentage: 1.0, @@ -46,11 +50,11 @@ export default { props: { collection: { type: Object, - default: () => {}, + default: () => ({}), }, chartOptions: { type: Object, - default: () => {}, + default: () => ({}), }, }, mounted() { diff --git a/app/javascript/dashboard/helper/auditlogHelper.js b/app/javascript/dashboard/helper/auditlogHelper.js index 7f912fb95..467b416f9 100644 --- a/app/javascript/dashboard/helper/auditlogHelper.js +++ b/app/javascript/dashboard/helper/auditlogHelper.js @@ -30,6 +30,7 @@ const translationKeys = { 'accountuser:create': `AUDIT_LOGS.ACCOUNT_USER.ADD`, 'accountuser:update:self': `AUDIT_LOGS.ACCOUNT_USER.EDIT.SELF`, 'accountuser:update:other': `AUDIT_LOGS.ACCOUNT_USER.EDIT.OTHER`, + 'account:update': `AUDIT_LOGS.ACCOUNT.EDIT`, }; function extractAttrChange(attrChange) { diff --git a/app/javascript/dashboard/i18n/locale/en/auditLogs.json b/app/javascript/dashboard/i18n/locale/en/auditLogs.json index a1c64b51e..f3e8edc2c 100644 --- a/app/javascript/dashboard/i18n/locale/en/auditLogs.json +++ b/app/javascript/dashboard/i18n/locale/en/auditLogs.json @@ -55,6 +55,9 @@ "ADD": "%{agentName} created a new macro (#%{id})", "EDIT": "%{agentName} updated a macro (#%{id})", "DELETE": "%{agentName} deleted a macro (#%{id})" + }, + "ACCOUNT": { + "EDIT": "%{agentName} updated the account configuration (#%{id})" } } } diff --git a/app/javascript/dashboard/i18n/locale/en/report.json b/app/javascript/dashboard/i18n/locale/en/report.json index 5858605a0..f384adad7 100644 --- a/app/javascript/dashboard/i18n/locale/en/report.json +++ b/app/javascript/dashboard/i18n/locale/en/report.json @@ -12,11 +12,11 @@ "DESC": "( Total )" }, "INCOMING_MESSAGES": { - "NAME": "Incoming Messages", + "NAME": "Messages received", "DESC": "( Total )" }, "OUTGOING_MESSAGES": { - "NAME": "Outgoing Messages", + "NAME": "Messages sent", "DESC": "( Total )" }, "FIRST_RESPONSE_TIME": { @@ -93,7 +93,6 @@ { "id": 3, "groupBy": "Month" } ], "GROUP_BY_YEAR_OPTIONS": [ - { "id": 1, "groupBy": "Day" }, { "id": 2, "groupBy": "Week" }, { "id": 3, "groupBy": "Month" }, { "id": 4, "groupBy": "Year" } diff --git a/app/javascript/dashboard/mixins/reportMixin.js b/app/javascript/dashboard/mixins/reportMixin.js index d5bb8c9cc..3a53f1196 100644 --- a/app/javascript/dashboard/mixins/reportMixin.js +++ b/app/javascript/dashboard/mixins/reportMixin.js @@ -10,42 +10,36 @@ export default { calculateTrend() { return metric_key => { if (!this.accountSummary.previous[metric_key]) return 0; + const diff = + this.accountSummary[metric_key] - + this.accountSummary.previous[metric_key]; return Math.round( - ((this.accountSummary[metric_key] - - this.accountSummary.previous[metric_key]) / - this.accountSummary.previous[metric_key]) * - 100 - ); - }; - }, - displayMetric() { - return metric_key => { - if (this.isAverageMetricType(metric_key)) { - return formatTime(this.accountSummary[metric_key]); - } - return this.accountSummary[metric_key]; - }; - }, - displayInfoText() { - return metric_key => { - if (this.metrics[this.currentSelection].KEY !== metric_key) { - return ''; - } - if (this.isAverageMetricType(metric_key)) { - const total = this.accountReport.data - .map(item => item.count) - .reduce((prev, curr) => prev + curr, 0); - return `${this.metrics[this.currentSelection].INFO_TEXT} ${total}`; - } - return ''; - }; - }, - isAverageMetricType() { - return metric_key => { - return ['avg_first_response_time', 'avg_resolution_time'].includes( - metric_key + (diff / this.accountSummary.previous[metric_key]) * 100 ); }; }, }, + methods: { + displayMetric(key) { + if (this.isAverageMetricType(key)) { + return formatTime(this.accountSummary[key]); + } + return Number(this.accountSummary[key] || '').toLocaleString(); + }, + displayInfoText(key) { + if (this.metrics[this.currentSelection].KEY !== key) { + return ''; + } + if (this.isAverageMetricType(key)) { + const total = this.accountReport.data + .map(item => item.count) + .reduce((prev, curr) => prev + curr, 0); + return `${this.metrics[this.currentSelection].INFO_TEXT} ${total}`; + } + return ''; + }, + isAverageMetricType(key) { + return ['avg_first_response_time', 'avg_resolution_time'].includes(key); + }, + }, }; diff --git a/app/javascript/dashboard/mixins/specs/reportMixin.spec.js b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js index aa3c451d9..d981de4e9 100644 --- a/app/javascript/dashboard/mixins/specs/reportMixin.spec.js +++ b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js @@ -23,7 +23,7 @@ describe('reportMixin', () => { mixins: [reportMixin], }; const wrapper = shallowMount(Component, { store, localVue }); - expect(wrapper.vm.displayMetric('conversations_count')).toEqual(5); + expect(wrapper.vm.displayMetric('conversations_count')).toEqual('5,000'); expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual( '3 Min 18 Sec' ); @@ -36,7 +36,7 @@ describe('reportMixin', () => { mixins: [reportMixin], }; const wrapper = shallowMount(Component, { store, localVue }); - expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(25); + expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(124900); expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0); }); diff --git a/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js b/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js index 8402c3940..ab6b6fecf 100644 --- a/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js +++ b/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js @@ -2,7 +2,7 @@ export default { summary: { avg_first_response_time: '198.6666666666667', avg_resolution_time: '208.3333333333333', - conversations_count: 5, + conversations_count: 5000, incoming_messages_count: 5, outgoing_messages_count: 3, previous: { diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue index d38e983a7..b04216bff 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue @@ -13,36 +13,7 @@ :show-group-by-filter="true" @filter-change="onFilterChange" /> -
- {{ $t('AGENT_REPORTS.FILTER_DROPDOWN_LABEL') }} -
-{{ props.option.name }}
-- {{ $t('LABEL_REPORTS.FILTER_DROPDOWN_LABEL') }} -
-+ {{ $t('AGENT_REPORTS.FILTER_DROPDOWN_LABEL') }} +
+{{ props.option.name }}
++ {{ $t('LABEL_REPORTS.FILTER_DROPDOWN_LABEL') }} +
++ + {{ $t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL') }} + + + {{ $t('TEAM_REPORTS.FILTER_DROPDOWN_LABEL') }} + + + + {{ $t('FORMS.MULTISELECT.SELECT_ONE') }} + +
++ {{ $t('REPORT.DURATION_FILTER_LABEL') }} +
++ {{ $t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER') }} +
++ {{ $t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL') }} +
+- - {{ $t('INBOX_REPORTS.FILTER_DROPDOWN_LABEL') }} - - - {{ $t('TEAM_REPORTS.FILTER_DROPDOWN_LABEL') }} - - - - {{ $t('FORMS.MULTISELECT.SELECT_ONE') }} - -
-- {{ $t('REPORT.DURATION_FILTER_LABEL') }} -
-- {{ $t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER') }} -
-- {{ $t('REPORT.GROUP_BY_FILTER_DROPDOWN_LABEL') }} -
-