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 @@ - - - {{ headerTitle }} - - - + + + + + + + + + {{ headerTitle }} + + + {{ headerDescription }} + + + + + + + + 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 @@ + + + + + {{ row.original.name }} + + 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 { - + - id => { + return $state.records.find(record => record.id === Number(id)) || {}; + }, getAgentStatus($state) { let status = { online: $state.records.filter( diff --git a/app/javascript/dashboard/store/modules/specs/summaryReports.spec.js b/app/javascript/dashboard/store/modules/specs/summaryReports.spec.js new file mode 100644 index 000000000..043987136 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/summaryReports.spec.js @@ -0,0 +1,189 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import SummaryReportsAPI from 'dashboard/api/summaryReports'; +import store, { initialState } from '../summaryReports'; + +vi.mock('dashboard/api/summaryReports', () => ({ + default: { + getInboxReports: vi.fn(), + getAgentReports: vi.fn(), + getTeamReports: vi.fn(), + }, +})); + +describe('Summary Reports Store', () => { + let commit; + + beforeEach(() => { + // Reset all mocks before each test + vi.clearAllMocks(); + commit = vi.fn(); + }); + + describe('Initial State', () => { + it('should have the correct initial state structure', () => { + expect(initialState).toEqual({ + inboxSummaryReports: [], + agentSummaryReports: [], + teamSummaryReports: [], + uiFlags: { + isFetchingInboxSummaryReports: false, + isFetchingAgentSummaryReports: false, + isFetchingTeamSummaryReports: false, + }, + }); + }); + }); + + describe('Getters', () => { + const state = { + inboxSummaryReports: [{ id: 1 }], + agentSummaryReports: [{ id: 2 }], + teamSummaryReports: [{ id: 3 }], + uiFlags: { isFetchingInboxSummaryReports: true }, + }; + + it('should return inbox summary reports', () => { + expect(store.getters.getInboxSummaryReports(state)).toEqual([{ id: 1 }]); + }); + + it('should return agent summary reports', () => { + expect(store.getters.getAgentSummaryReports(state)).toEqual([{ id: 2 }]); + }); + + it('should return team summary reports', () => { + expect(store.getters.getTeamSummaryReports(state)).toEqual([{ id: 3 }]); + }); + + it('should return UI flags', () => { + expect(store.getters.getUIFlags(state)).toEqual({ + isFetchingInboxSummaryReports: true, + }); + }); + }); + + describe('Mutations', () => { + it('should set inbox summary report', () => { + const state = { ...initialState }; + const data = [{ id: 1 }]; + + store.mutations.setInboxSummaryReport(state, data); + expect(state.inboxSummaryReports).toEqual(data); + }); + + it('should set agent summary report', () => { + const state = { ...initialState }; + const data = [{ id: 2 }]; + + store.mutations.setAgentSummaryReport(state, data); + expect(state.agentSummaryReports).toEqual(data); + }); + + it('should set team summary report', () => { + const state = { ...initialState }; + const data = [{ id: 3 }]; + + store.mutations.setTeamSummaryReport(state, data); + expect(state.teamSummaryReports).toEqual(data); + }); + + it('should merge UI flags with existing flags', () => { + const state = { + uiFlags: { flag1: true, flag2: false }, + }; + const newFlags = { flag2: true, flag3: true }; + + store.mutations.setUIFlags(state, newFlags); + expect(state.uiFlags).toEqual({ + flag1: true, + flag2: true, + flag3: true, + }); + }); + }); + + describe('Actions', () => { + describe('fetchInboxSummaryReports', () => { + it('should fetch inbox reports successfully', async () => { + const params = { date: '2025-01-01' }; + const mockResponse = { + data: [{ report_id: 1, report_name: 'Test' }], + }; + + SummaryReportsAPI.getInboxReports.mockResolvedValue(mockResponse); + + await store.actions.fetchInboxSummaryReports({ commit }, params); + + expect(commit).toHaveBeenCalledWith('setUIFlags', { + isFetchingInboxSummaryReports: true, + }); + expect(SummaryReportsAPI.getInboxReports).toHaveBeenCalledWith(params); + expect(commit).toHaveBeenCalledWith('setInboxSummaryReport', [ + { reportId: 1, reportName: 'Test' }, + ]); + expect(commit).toHaveBeenCalledWith('setUIFlags', { + isFetchingInboxSummaryReports: false, + }); + }); + + it('should handle errors gracefully', async () => { + SummaryReportsAPI.getInboxReports.mockRejectedValue( + new Error('API Error') + ); + + await store.actions.fetchInboxSummaryReports({ commit }, {}); + + expect(commit).toHaveBeenCalledWith('setUIFlags', { + isFetchingInboxSummaryReports: false, + }); + }); + }); + + describe('fetchAgentSummaryReports', () => { + it('should fetch agent reports successfully', async () => { + const params = { agentId: 123 }; + const mockResponse = { + data: [{ agent_id: 123, agent_name: 'Test Agent' }], + }; + + SummaryReportsAPI.getAgentReports.mockResolvedValue(mockResponse); + + await store.actions.fetchAgentSummaryReports({ commit }, params); + + expect(commit).toHaveBeenCalledWith('setUIFlags', { + isFetchingAgentSummaryReports: true, + }); + expect(SummaryReportsAPI.getAgentReports).toHaveBeenCalledWith(params); + expect(commit).toHaveBeenCalledWith('setAgentSummaryReport', [ + { agentId: 123, agentName: 'Test Agent' }, + ]); + expect(commit).toHaveBeenCalledWith('setUIFlags', { + isFetchingAgentSummaryReports: false, + }); + }); + }); + + describe('fetchTeamSummaryReports', () => { + it('should fetch team reports successfully', async () => { + const params = { teamId: 456 }; + const mockResponse = { + data: [{ team_id: 456, team_name: 'Test Team' }], + }; + + SummaryReportsAPI.getTeamReports.mockResolvedValue(mockResponse); + + await store.actions.fetchTeamSummaryReports({ commit }, params); + + expect(commit).toHaveBeenCalledWith('setUIFlags', { + isFetchingTeamSummaryReports: true, + }); + expect(SummaryReportsAPI.getTeamReports).toHaveBeenCalledWith(params); + expect(commit).toHaveBeenCalledWith('setTeamSummaryReport', [ + { teamId: 456, teamName: 'Test Team' }, + ]); + expect(commit).toHaveBeenCalledWith('setUIFlags', { + isFetchingTeamSummaryReports: false, + }); + }); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/summaryReports.js b/app/javascript/dashboard/store/modules/summaryReports.js new file mode 100644 index 000000000..15df7589b --- /dev/null +++ b/app/javascript/dashboard/store/modules/summaryReports.js @@ -0,0 +1,98 @@ +import SummaryReportsAPI from 'dashboard/api/summaryReports'; +import camelcaseKeys from 'camelcase-keys'; + +const typeMap = { + inbox: { + flagKey: 'isFetchingInboxSummaryReports', + apiMethod: 'getInboxReports', + mutationKey: 'setInboxSummaryReport', + }, + agent: { + flagKey: 'isFetchingAgentSummaryReports', + apiMethod: 'getAgentReports', + mutationKey: 'setAgentSummaryReport', + }, + team: { + flagKey: 'isFetchingTeamSummaryReports', + apiMethod: 'getTeamReports', + mutationKey: 'setTeamSummaryReport', + }, +}; + +async function fetchSummaryReports(type, params, { commit }) { + const config = typeMap[type]; + if (!config) return; + + try { + commit('setUIFlags', { [config.flagKey]: true }); + const response = await SummaryReportsAPI[config.apiMethod](params); + commit(config.mutationKey, camelcaseKeys(response.data, { deep: true })); + } catch (error) { + // Ignore error + } finally { + commit('setUIFlags', { [config.flagKey]: false }); + } +} + +export const initialState = { + inboxSummaryReports: [], + agentSummaryReports: [], + teamSummaryReports: [], + uiFlags: { + isFetchingInboxSummaryReports: false, + isFetchingAgentSummaryReports: false, + isFetchingTeamSummaryReports: false, + }, +}; + +export const getters = { + getInboxSummaryReports(state) { + return state.inboxSummaryReports; + }, + getAgentSummaryReports(state) { + return state.agentSummaryReports; + }, + getTeamSummaryReports(state) { + return state.teamSummaryReports; + }, + getUIFlags(state) { + return state.uiFlags; + }, +}; + +export const actions = { + fetchInboxSummaryReports({ commit }, params) { + return fetchSummaryReports('inbox', params, { commit }); + }, + + fetchAgentSummaryReports({ commit }, params) { + return fetchSummaryReports('agent', params, { commit }); + }, + + fetchTeamSummaryReports({ commit }, params) { + return fetchSummaryReports('team', params, { commit }); + }, +}; + +export const mutations = { + setInboxSummaryReport(state, data) { + state.inboxSummaryReports = data; + }, + setAgentSummaryReport(state, data) { + state.agentSummaryReports = data; + }, + setTeamSummaryReport(state, data) { + state.teamSummaryReports = data; + }, + setUIFlags(state, uiFlag) { + state.uiFlags = { ...state.uiFlags, ...uiFlag }; + }, +}; + +export default { + namespaced: true, + state: initialState, + getters, + actions, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/teams/getters.js b/app/javascript/dashboard/store/modules/teams/getters.js index 0a49fcfcd..139407b21 100644 --- a/app/javascript/dashboard/store/modules/teams/getters.js +++ b/app/javascript/dashboard/store/modules/teams/getters.js @@ -2,6 +2,12 @@ export const getters = { getTeams($state) { return Object.values($state.records).sort((a, b) => a.id - b.id); }, + getTeamById: $state => id => { + return ( + Object.values($state.records).find(record => record.id === Number(id)) || + {} + ); + }, getMyTeams($state, $getters) { return $getters.getTeams.filter(team => { const { is_member: isMember } = team; diff --git a/app/presenters/reports/time_format_presenter.rb b/app/presenters/reports/time_format_presenter.rb index b69618499..e24367c4c 100644 --- a/app/presenters/reports/time_format_presenter.rb +++ b/app/presenters/reports/time_format_presenter.rb @@ -3,12 +3,12 @@ class Reports::TimeFormatPresenter attr_reader :seconds - def initialize(seconds) - @seconds = seconds.to_i + def initialize(seconds = nil) + @seconds = seconds.to_i if seconds.present? end def format - return '--' if seconds.nil? || seconds.zero? + return 'N/A' if seconds.nil? || seconds.zero? days, remainder = seconds.divmod(86_400) hours, remainder = remainder.divmod(3600) diff --git a/config/features.yml b/config/features.yml index 2cc15983c..8fdab0e3b 100644 --- a/config/features.yml +++ b/config/features.yml @@ -94,3 +94,5 @@ premium: true - name: chatwoot_v4 enabled: false +- name: report_v4 + enabled: false diff --git a/spec/presenters/reports/time_format_presenter_spec.rb b/spec/presenters/reports/time_format_presenter_spec.rb index 64d75bb08..da50f55dd 100644 --- a/spec/presenters/reports/time_format_presenter_spec.rb +++ b/spec/presenters/reports/time_format_presenter_spec.rb @@ -68,6 +68,10 @@ RSpec.describe Reports::TimeFormatPresenter do it 'formats single second correctly' do expect(described_class.new(1).format).to eq '1 second' end + + it 'formats nil second correctly' do + expect(described_class.new.format).to eq 'N/A' + end end end end
+ {{ headerDescription }} +