From cb42be8e6596f91f65145143beffaac04cea7543 Mon Sep 17 00:00:00 2001 From: Pranav Date: Mon, 27 Jan 2025 19:49:18 -0800 Subject: [PATCH] feat(v4): Update the report pages to show aggregate values (#10766) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates the report pages for agents, inboxes, and teams by replacing charts with aggregate values (under a feature flag). Users can click on any item to view more details if needed. Most users seem to prefer aggregate values, so this change will likely stay. The PR also includes a few fixes: - The summary reports now use the same logic for both the front-end and CSV exports. - Fixed an issue where a single quote was being added to values with hyphens in CSV files. Now, ‘n/a’ is used when no value is available. - Fixed a bug where the average value was calculated incorrectly when multiple accounts were present. These changes should make reports easier to use and more consistent. ### Agents: Screenshot 2025-01-26 at 10 47 18 AM ### Inboxes Screenshot 2025-01-26 at 10 47 10 AM ### Teams: Screenshot 2025-01-26 at 10 47 01 AM --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Shivam Mishra --- .../timeseries/average_report_builder.rb | 2 +- app/helpers/api/v2/accounts/reports_helper.rb | 66 +++--- .../dashboard/api/summaryReports.js | 40 ++++ .../components-next/sidebar/Sidebar.vue | 87 ++++++-- .../layout/config/sidebarItems/reports.js | 1 + .../dashboard/components/table/Table.vue | 2 +- .../components/widgets/BackButton.vue | 6 +- app/javascript/dashboard/featureFlags.js | 1 + .../dashboard/i18n/locale/en/report.json | 13 ++ .../settings/reports/AgentReportsIndex.vue | 35 ++++ .../settings/reports/AgentReportsShow.vue | 31 +++ .../settings/reports/InboxReportsIndex.vue | 35 ++++ .../settings/reports/InboxReportsShow.vue | 27 +++ .../settings/reports/TeamReportsIndex.vue | 35 ++++ .../settings/reports/TeamReportsShow.vue | 27 +++ .../reports/components/ReportFilters.vue | 10 +- .../reports/components/ReportHeader.vue | 36 +++- .../reports/components/SummaryReportLink.vue | 20 ++ .../reports/components/SummaryReports.vue | 184 +++++++++++++++++ .../reports/components/WootReports.vue | 14 +- .../settings/reports/reports.routes.js | 133 +++++++++--- app/javascript/dashboard/store/index.js | 20 +- .../dashboard/store/modules/agents.js | 3 + .../modules/specs/summaryReports.spec.js | 189 ++++++++++++++++++ .../dashboard/store/modules/summaryReports.js | 98 +++++++++ .../dashboard/store/modules/teams/getters.js | 6 + .../reports/time_format_presenter.rb | 6 +- config/features.yml | 2 + .../reports/time_format_presenter_spec.rb | 4 + 29 files changed, 1026 insertions(+), 107 deletions(-) create mode 100644 app/javascript/dashboard/api/summaryReports.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/AgentReportsIndex.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/AgentReportsShow.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/InboxReportsIndex.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/InboxReportsShow.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/TeamReportsIndex.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/TeamReportsShow.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/SummaryReportLink.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/SummaryReports.vue create mode 100644 app/javascript/dashboard/store/modules/specs/summaryReports.spec.js create mode 100644 app/javascript/dashboard/store/modules/summaryReports.js diff --git a/app/builders/v2/reports/timeseries/average_report_builder.rb b/app/builders/v2/reports/timeseries/average_report_builder.rb index 3e30557e1..5df718b6a 100644 --- a/app/builders/v2/reports/timeseries/average_report_builder.rb +++ b/app/builders/v2/reports/timeseries/average_report_builder.rb @@ -28,7 +28,7 @@ class V2::Reports::Timeseries::AverageReportBuilder < V2::Reports::Timeseries::B end def object_scope - scope.reporting_events.where(name: event_name, created_at: range) + scope.reporting_events.where(name: event_name, created_at: range, account_id: account.id) end def reporting_events diff --git a/app/helpers/api/v2/accounts/reports_helper.rb b/app/helpers/api/v2/accounts/reports_helper.rb index 4d5a6f115..22c51b6ef 100644 --- a/app/helpers/api/v2/accounts/reports_helper.rb +++ b/app/helpers/api/v2/accounts/reports_helper.rb @@ -1,58 +1,70 @@ module Api::V2::Accounts::ReportsHelper def generate_agents_report + reports = V2::Reports::AgentSummaryBuilder.new( + account: Current.account, + params: build_params(type: :agent) + ).build + Current.account.users.map do |agent| - agent_report = report_builder({ type: :agent, id: agent.id }).summary - [agent.name] + generate_readable_report_metrics(agent_report) + report = reports.find { |r| r[:id] == agent.id } + [agent.name] + generate_readable_report_metrics(report) end end def generate_inboxes_report + reports = V2::Reports::InboxSummaryBuilder.new( + account: Current.account, + params: build_params(type: :inbox) + ).build + Current.account.inboxes.map do |inbox| - inbox_report = generate_report({ type: :inbox, id: inbox.id }) - [inbox.name, inbox.channel&.name] + generate_readable_report_metrics(inbox_report) + report = reports.find { |r| r[:id] == inbox.id } + [inbox.name, inbox.channel&.name] + generate_readable_report_metrics(report) end end def generate_teams_report + reports = V2::Reports::TeamSummaryBuilder.new( + account: Current.account, + params: build_params(type: :team) + ).build + Current.account.teams.map do |team| - team_report = report_builder({ type: :team, id: team.id }).summary - [team.name] + generate_readable_report_metrics(team_report) + report = reports.find { |r| r[:id] == team.id } + [team.name] + generate_readable_report_metrics(report) end end def generate_labels_report Current.account.labels.map do |label| - label_report = generate_report({ type: :label, id: label.id }) + label_report = report_builder({ type: :label, id: label.id }).short_summary [label.title] + generate_readable_report_metrics(label_report) end end - def report_builder(report_params) - V2::ReportBuilder.new( - Current.account, - report_params.merge( - { - since: params[:since], - until: params[:until], - business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours]) - } - ) + private + + def build_params(base_params) + base_params.merge( + { + since: params[:since], + until: params[:until], + business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours]) + } ) end - def generate_report(report_params) - report_builder(report_params).short_summary + def report_builder(report_params) + V2::ReportBuilder.new(Current.account, build_params(report_params)) end - private - - def generate_readable_report_metrics(report_metric) + def generate_readable_report_metrics(report) [ - report_metric[:conversations_count], - Reports::TimeFormatPresenter.new(report_metric[:avg_first_response_time]).format, - Reports::TimeFormatPresenter.new(report_metric[:avg_resolution_time]).format, - Reports::TimeFormatPresenter.new(report_metric[:reply_time]).format, - report_metric[:resolutions_count] + report[:conversations_count], + Reports::TimeFormatPresenter.new(report[:avg_first_response_time]).format, + Reports::TimeFormatPresenter.new(report[:avg_resolution_time]).format, + Reports::TimeFormatPresenter.new(report[:avg_reply_time]).format, + report[:resolved_conversations_count] ] end end diff --git a/app/javascript/dashboard/api/summaryReports.js b/app/javascript/dashboard/api/summaryReports.js new file mode 100644 index 000000000..f772ef86f --- /dev/null +++ b/app/javascript/dashboard/api/summaryReports.js @@ -0,0 +1,40 @@ +/* global axios */ +import ApiClient from './ApiClient'; + +class SummaryReportsAPI extends ApiClient { + constructor() { + super('summary_reports', { accountScoped: true, apiVersion: 'v2' }); + } + + getTeamReports({ since, until, businessHours } = {}) { + return axios.get(`${this.url}/team`, { + params: { + since, + until, + business_hours: businessHours, + }, + }); + } + + getAgentReports({ since, until, businessHours } = {}) { + return axios.get(`${this.url}/agent`, { + params: { + since, + until, + business_hours: businessHours, + }, + }); + } + + getInboxReports({ since, until, businessHours } = {}) { + return axios.get(`${this.url}/inbox`, { + params: { + since, + until, + business_hours: businessHours, + }, + }); + } +} + +export default new SummaryReportsAPI(); diff --git a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue index 3f2f4ed6f..dcf4e9740 100644 --- a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue +++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue @@ -8,6 +8,7 @@ import { useStore } from 'vuex'; import { useI18n } from 'vue-i18n'; import { useStorage } from '@vueuse/core'; import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts'; +import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import Button from 'dashboard/components-next/button/Button.vue'; import SidebarGroup from './SidebarGroup.vue'; @@ -36,6 +37,18 @@ const toggleShortcutModalFn = show => { } }; +const currentAccountId = useMapGetter('getCurrentAccountId'); +const isFeatureEnabledonAccount = useMapGetter( + 'accounts/isFeatureEnabledonAccount' +); + +const showV4Routes = computed(() => { + return isFeatureEnabledonAccount.value( + currentAccountId.value, + FEATURE_FLAGS.REPORT_V4 + ); +}); + useSidebarKeyboardShortcuts(toggleShortcutModalFn); // We're using localStorage to store the expanded item in the sidebar @@ -77,6 +90,59 @@ const sortedInboxes = computed(() => inboxes.value.slice().sort((a, b) => a.name.localeCompare(b.name)) ); +const newReportRoutes = [ + { + name: 'Reports Agent', + label: t('SIDEBAR.REPORTS_AGENT'), + to: accountScopedRoute('agent_reports_index'), + activeOn: ['agent_reports_show'], + }, + { + name: 'Reports Label', + label: t('SIDEBAR.REPORTS_LABEL'), + to: accountScopedRoute('label_reports'), + }, + { + name: 'Reports Inbox', + label: t('SIDEBAR.REPORTS_INBOX'), + to: accountScopedRoute('inbox_reports_index'), + activeOn: ['inbox_reports_show'], + }, + { + name: 'Reports Team', + label: t('SIDEBAR.REPORTS_TEAM'), + to: accountScopedRoute('team_reports_index'), + activeOn: ['team_reports_show'], + }, +]; + +const oldReportRoutes = [ + { + name: 'Reports Agent', + label: t('SIDEBAR.REPORTS_AGENT'), + to: accountScopedRoute('agent_reports'), + }, + { + name: 'Reports Label', + label: t('SIDEBAR.REPORTS_LABEL'), + to: accountScopedRoute('label_reports'), + }, + { + name: 'Reports Inbox', + label: t('SIDEBAR.REPORTS_INBOX'), + to: accountScopedRoute('inbox_reports'), + }, + { + name: 'Reports Team', + label: t('SIDEBAR.REPORTS_TEAM'), + to: accountScopedRoute('team_reports'), + }, +]; + +const reportRoutes = computed(() => + showV4Routes.value ? newReportRoutes : oldReportRoutes +); + const menuItems = computed(() => { return [ { @@ -265,31 +331,12 @@ const menuItems = computed(() => { label: t('SIDEBAR.REPORTS_CONVERSATION'), to: accountScopedRoute('conversation_reports'), }, + ...reportRoutes.value, { name: 'Reports CSAT', label: t('SIDEBAR.CSAT'), to: accountScopedRoute('csat_reports'), }, - { - name: 'Reports Agent', - label: t('SIDEBAR.REPORTS_AGENT'), - to: accountScopedRoute('agent_reports'), - }, - { - name: 'Reports Label', - label: t('SIDEBAR.REPORTS_LABEL'), - to: accountScopedRoute('label_reports'), - }, - { - name: 'Reports Inbox', - label: t('SIDEBAR.REPORTS_INBOX'), - to: accountScopedRoute('inbox_reports'), - }, - { - name: 'Reports Team', - label: t('SIDEBAR.REPORTS_TEAM'), - to: accountScopedRoute('team_reports'), - }, { name: 'Reports SLA', label: t('SIDEBAR.REPORTS_SLA'), diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js b/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js index e7e4997fd..f9f00b88d 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js @@ -11,6 +11,7 @@ const reports = accountId => ({ 'agent_reports', 'label_reports', 'inbox_reports', + 'inbox_reports_show', 'team_reports', 'sla_reports', ], diff --git a/app/javascript/dashboard/components/table/Table.vue b/app/javascript/dashboard/components/table/Table.vue index 5936fa314..81edc4840 100644 --- a/app/javascript/dashboard/components/table/Table.vue +++ b/app/javascript/dashboard/components/table/Table.vue @@ -40,7 +40,7 @@ const headerClass = computed(() => :style="{ width: `${header.getSize()}px`, }" - class="text-left py-3 px-5 font-normal text-sm" + class="text-left py-3 px-5 font-medium text-sm text-n-slate-12" :class="headerClass" @click="header.column.getCanSort() && header.column.toggleSorting()" > diff --git a/app/javascript/dashboard/components/widgets/BackButton.vue b/app/javascript/dashboard/components/widgets/BackButton.vue index 9a5557f0b..09a22ffa3 100644 --- a/app/javascript/dashboard/components/widgets/BackButton.vue +++ b/app/javascript/dashboard/components/widgets/BackButton.vue @@ -37,8 +37,10 @@ const buttonStyleClass = props.compact > {{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }} diff --git a/app/javascript/dashboard/featureFlags.js b/app/javascript/dashboard/featureFlags.js index 4417ef164..094fbfa27 100644 --- a/app/javascript/dashboard/featureFlags.js +++ b/app/javascript/dashboard/featureFlags.js @@ -33,4 +33,5 @@ export const FEATURE_FLAGS = { CAPTAIN: 'captain_integration', CUSTOM_ROLES: 'custom_roles', CHATWOOT_V4: 'chatwoot_v4', + REPORT_V4: 'report_v4', }; diff --git a/app/javascript/dashboard/i18n/locale/en/report.json b/app/javascript/dashboard/i18n/locale/en/report.json index a6389f017..c9ac00f7f 100644 --- a/app/javascript/dashboard/i18n/locale/en/report.json +++ b/app/javascript/dashboard/i18n/locale/en/report.json @@ -124,6 +124,7 @@ }, "AGENT_REPORTS": { "HEADER": "Agents Overview", + "DESCRIPTION": "Easily track agent performance with key metrics such as conversations, response times, resolution times, and resolved cases. Click an agent’s name to learn more.", "LOADING_CHART": "Loading chart data...", "NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.", "DOWNLOAD_AGENT_REPORTS": "Download agent reports", @@ -258,6 +259,7 @@ }, "INBOX_REPORTS": { "HEADER": "Inbox Overview", + "DESCRIPTION": "Quickly view your inbox performance with key metrics like conversations, response times, resolution times, and resolved cases—all in one place. Click an inbox name for more details.", "LOADING_CHART": "Loading chart data...", "NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.", "DOWNLOAD_INBOX_REPORTS": "Download inbox reports", @@ -325,6 +327,7 @@ }, "TEAM_REPORTS": { "HEADER": "Team Overview", + "DESCRIPTION": "Get a snapshot of your team’s performance with essential metrics, including conversations, response times, resolution times, and resolved cases. Click a team name for more details.", "LOADING_CHART": "Loading chart data...", "NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.", "DOWNLOAD_TEAM_REPORTS": "Download team reports", @@ -538,5 +541,15 @@ }, "VIEW_DETAILS": "View Details" } + }, + "SUMMARY_REPORTS": { + "INBOX": "Inbox", + "AGENT": "Agent", + "TEAM": "Team", + "AVG_RESOLUTION_TIME": "Avg. Resolution Time", + "AVG_FIRST_RESPONSE_TIME": "Avg. First Response Time", + "AVG_REPLY_TIME": "Avg. Customer Waiting Time", + "RESOLUTION_COUNT": "Resolution Count", + "CONVERSATIONS": "No. of conversations" } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/AgentReportsIndex.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/AgentReportsIndex.vue new file mode 100644 index 000000000..b0993e93b --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/AgentReportsIndex.vue @@ -0,0 +1,35 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/AgentReportsShow.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/AgentReportsShow.vue new file mode 100644 index 000000000..211e39167 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/AgentReportsShow.vue @@ -0,0 +1,31 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/InboxReportsIndex.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/InboxReportsIndex.vue new file mode 100644 index 000000000..caf43ae20 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/InboxReportsIndex.vue @@ -0,0 +1,35 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/InboxReportsShow.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/InboxReportsShow.vue new file mode 100644 index 000000000..06b584aae --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/InboxReportsShow.vue @@ -0,0 +1,27 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/TeamReportsIndex.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/TeamReportsIndex.vue new file mode 100644 index 000000000..141243ee9 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/TeamReportsIndex.vue @@ -0,0 +1,35 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/TeamReportsShow.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/TeamReportsShow.vue new file mode 100644 index 000000000..9f346a772 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/TeamReportsShow.vue @@ -0,0 +1,27 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue index 053ae7cd7..5950edfd9 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue @@ -15,6 +15,10 @@ export default { Thumbnail, }, props: { + currentFilter: { + type: Object, + default: () => null, + }, filterItemsList: { type: Array, default: () => [], @@ -40,7 +44,7 @@ export default { ], data() { return { - currentSelectedFilter: null, + currentSelectedFilter: this.currentFilter || null, currentDateRangeSelection: { id: 0, name: this.$t('REPORT.DATE_RANGE_OPTIONS.LAST_7_DAYS'), @@ -113,7 +117,9 @@ export default { }, watch: { filterItemsList(val) { - this.currentSelectedFilter = val[0]; + this.currentSelectedFilter = !this.currentFilter + ? val[0] + : this.currentFilter; this.changeFilterSelection(); }, groupByFilterItemsList() { diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportHeader.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportHeader.vue index a42442a9d..c7643a5fe 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportHeader.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportHeader.vue @@ -1,17 +1,41 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/SummaryReportLink.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/SummaryReportLink.vue new file mode 100644 index 000000000..fdb201d25 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/SummaryReportLink.vue @@ -0,0 +1,20 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/SummaryReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/SummaryReports.vue new file mode 100644 index 000000000..4c0c4fe64 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/SummaryReports.vue @@ -0,0 +1,184 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue index b0e6b84a0..b3d66a8eb 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue @@ -54,12 +54,20 @@ export default { type: String, default: 'Download Reports', }, + hasBackButton: { + type: Boolean, + default: false, + }, + selectedItem: { + type: Object, + default: null, + }, }, data() { return { from: 0, to: 0, - selectedFilter: null, + selectedFilter: this.selectedItem, groupBy: GROUP_BY_FILTER[1], groupByfilterItemsList: GROUP_BY_OPTIONS.DAY.map(this.translateOptions), selectedGroupByFilter: null, @@ -206,7 +214,7 @@ export default {