diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 71e9100e7..729db34b5 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -65,6 +65,10 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController contacts = result[:contacts] @contacts_count = result[:count] @contacts = fetch_contacts(contacts) + rescue CustomExceptions::CustomFilter::InvalidAttribute, + CustomExceptions::CustomFilter::InvalidOperator, + CustomExceptions::CustomFilter::InvalidValue => e + render_could_not_create_error(e.message) end def contactable_inboxes diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index d0d8f6d5b..2aedf1928 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -44,6 +44,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro result = ::Conversations::FilterService.new(params.permit!, current_user).perform @conversations = result[:conversations] @conversations_count = result[:count] + rescue CustomExceptions::CustomFilter::InvalidAttribute, + CustomExceptions::CustomFilter::InvalidOperator, + CustomExceptions::CustomFilter::InvalidValue => e + render_could_not_create_error(e.message) end def mute diff --git a/app/controllers/concerns/domain_helper.rb b/app/controllers/concerns/domain_helper.rb new file mode 100644 index 000000000..1b7d8f187 --- /dev/null +++ b/app/controllers/concerns/domain_helper.rb @@ -0,0 +1,5 @@ +module DomainHelper + def self.chatwoot_domain?(domain = request.host) + [URI.parse(ENV.fetch('FRONTEND_URL', '')).host, URI.parse(ENV.fetch('HELPCENTER_URL', '')).host].include?(domain) + end +end diff --git a/app/controllers/concerns/switch_locale.rb b/app/controllers/concerns/switch_locale.rb index 744a70da9..3013ff3cc 100644 --- a/app/controllers/concerns/switch_locale.rb +++ b/app/controllers/concerns/switch_locale.rb @@ -6,6 +6,7 @@ module SwitchLocale def switch_locale(&) # priority is for locale set in query string (mostly for widget/from js sdk) locale ||= locale_from_params + locale ||= locale_from_custom_domain # if locale is not set in account, let's use DEFAULT_LOCALE env variable locale ||= locale_from_env_variable set_locale(locale, &) @@ -16,6 +17,20 @@ module SwitchLocale set_locale(locale, &) end + # If the request is coming from a custom domain, it should be for a helpcenter portal + # We will use the portal locale in such cases + def locale_from_custom_domain(&) + return if params[:locale] + + domain = request.host + return if DomainHelper.chatwoot_domain?(domain) + + @portal = Portal.find_by(custom_domain: domain) + return unless @portal + + @portal.default_locale + end + def set_locale(locale, &) # if locale is empty, use default_locale locale ||= I18n.default_locale diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 0aea9df83..047fd10c3 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -18,6 +18,7 @@ class DashboardController < ActionController::Base 'LOGO', 'LOGO_DARK', 'LOGO_THUMBNAIL', 'INSTALLATION_NAME', 'WIDGET_BRAND_URL', 'TERMS_URL', + 'BRAND_URL', 'BRAND_NAME', 'PRIVACY_URL', 'DISPLAY_MANIFEST', 'CREATE_NEW_ACCOUNT_FROM_DASHBOARD', diff --git a/app/controllers/public/api/v1/portals/base_controller.rb b/app/controllers/public/api/v1/portals/base_controller.rb index 4d3cc56b8..f6c10f7c4 100644 --- a/app/controllers/public/api/v1/portals/base_controller.rb +++ b/app/controllers/public/api/v1/portals/base_controller.rb @@ -47,7 +47,7 @@ class Public::Api::V1::Portals::BaseController < PublicController @locale = if article.category.present? article.category.locale else - 'en' + article.portal.default_locale end I18n.with_locale(@locale, &) diff --git a/app/controllers/public_controller.rb b/app/controllers/public_controller.rb index 0c3f52ff6..3b83a2210 100644 --- a/app/controllers/public_controller.rb +++ b/app/controllers/public_controller.rb @@ -8,8 +8,7 @@ class PublicController < ActionController::Base def ensure_custom_domain_request domain = request.host - - return if [URI.parse(ENV.fetch('FRONTEND_URL', '')).host, URI.parse(ENV.fetch('HELPCENTER_URL', '')).host].include?(domain) + return if DomainHelper.chatwoot_domain?(domain) @portal = ::Portal.find_by(custom_domain: domain) return if @portal.present? diff --git a/app/helpers/filter_helper.rb b/app/helpers/filter_helper.rb new file mode 100644 index 000000000..bce2de5ea --- /dev/null +++ b/app/helpers/filter_helper.rb @@ -0,0 +1,84 @@ +module FilterHelper + def build_condition_query(model_filters, query_hash, current_index) + current_filter = model_filters[query_hash['attribute_key']] + + # Throw InvalidOperator Error if the attribute is a standard attribute + # and the operator is not allowed in the config + if current_filter.present? && current_filter['filter_operators'].exclude?(query_hash[:filter_operator]) + raise CustomExceptions::CustomFilter::InvalidOperator.new( + attribute_name: query_hash['attribute_key'], + allowed_keys: current_filter['filter_operators'] + ) + end + + # Every other filter expects a value to be present + if %w[is_present is_not_present].exclude?(query_hash[:filter_operator]) && query_hash['values'].blank? + raise CustomExceptions::CustomFilter::InvalidValue.new(attribute_name: query_hash['attribute_key']) + end + + condition_query = build_condition_query_string(current_filter, query_hash, current_index) + # The query becomes empty only when it doesn't match to any supported + # standard attribute or custom attribute defined in the account. + if condition_query.empty? + raise CustomExceptions::CustomFilter::InvalidAttribute.new(key: query_hash['attribute_key'], + allowed_keys: model_filters.keys) + end + + condition_query + end + + def build_condition_query_string(current_filter, query_hash, current_index) + filter_operator_value = filter_operation(query_hash, current_index) + + return handle_nil_filter(query_hash, current_index) if current_filter.nil? + + case current_filter['attribute_type'] + when 'additional_attributes' + handle_additional_attributes(query_hash, filter_operator_value, current_filter['data_type']) + else + handle_standard_attributes(current_filter, query_hash, current_index, filter_operator_value) + end + end + + def handle_nil_filter(query_hash, current_index) + attribute_type = "#{filter_config[:entity].downcase}_attribute" + custom_attribute_query(query_hash, attribute_type, current_index) + end + + def handle_additional_attributes(query_hash, filter_operator_value, data_type) + if data_type == 'text_case_insensitive' + "LOWER(#{filter_config[:table_name]}.additional_attributes ->> '#{query_hash[:attribute_key]}') " \ + "#{filter_operator_value} #{query_hash[:query_operator]}" + else + "#{filter_config[:table_name]}.additional_attributes ->> '#{query_hash[:attribute_key]}' " \ + "#{filter_operator_value} #{query_hash[:query_operator]} " + end + end + + def handle_standard_attributes(current_filter, query_hash, current_index, filter_operator_value) + case current_filter['data_type'] + when 'date' + date_filter(current_filter, query_hash, filter_operator_value) + when 'labels' + tag_filter_query(query_hash, current_index) + when 'text_case_insensitive' + text_case_insensitive_filter(query_hash, filter_operator_value) + else + default_filter(query_hash, filter_operator_value) + end + end + + def date_filter(current_filter, query_hash, filter_operator_value) + "(#{filter_config[:table_name]}.#{query_hash[:attribute_key]})::#{current_filter['data_type']} " \ + "#{filter_operator_value}#{current_filter['data_type']} #{query_hash[:query_operator]}" + end + + def text_case_insensitive_filter(query_hash, filter_operator_value) + "LOWER(#{filter_config[:table_name]}.#{query_hash[:attribute_key]}) " \ + "#{filter_operator_value} #{query_hash[:query_operator]}" + end + + def default_filter(query_hash, filter_operator_value) + "#{filter_config[:table_name]}.#{query_hash[:attribute_key]} #{filter_operator_value} #{query_hash[:query_operator]}" + end +end diff --git a/app/javascript/dashboard/api/reports.js b/app/javascript/dashboard/api/reports.js index 987b69701..52fa7f444 100644 --- a/app/javascript/dashboard/api/reports.js +++ b/app/javascript/dashboard/api/reports.js @@ -84,6 +84,24 @@ class ReportsAPI extends ApiClient { params: { since, until, business_hours: businessHours }, }); } + + getBotMetrics({ from, to } = {}) { + return axios.get(`${this.url}/bot_metrics`, { + params: { since: from, until: to }, + }); + } + + getBotSummary({ from, to, groupBy, businessHours } = {}) { + return axios.get(`${this.url}/bot_summary`, { + params: { + since: from, + until: to, + type: 'account', + group_by: groupBy, + business_hours: businessHours, + }, + }); + } } export default new ReportsAPI(); diff --git a/app/javascript/dashboard/api/specs/reports.spec.js b/app/javascript/dashboard/api/specs/reports.spec.js index 7822dad8f..05d4a152c 100644 --- a/app/javascript/dashboard/api/specs/reports.spec.js +++ b/app/javascript/dashboard/api/specs/reports.spec.js @@ -111,6 +111,40 @@ describe('#Reports API', () => { }); }); + it('#getBotMetrics', () => { + reportsAPI.getBotMetrics({ from: 1621103400, to: 1621621800 }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v2/reports/bot_metrics', + { + params: { + since: 1621103400, + until: 1621621800, + }, + } + ); + }); + + it('#getBotSummary', () => { + reportsAPI.getBotSummary({ + from: 1621103400, + to: 1621621800, + groupBy: 'date', + businessHours: true, + }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v2/reports/bot_summary', + { + params: { + since: 1621103400, + until: 1621621800, + type: 'account', + group_by: 'date', + business_hours: true, + }, + } + ); + }); + it('#getConversationMetric', () => { reportsAPI.getConversationMetric('account'); expect(axiosMock.get).toHaveBeenCalledWith( diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js b/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js index 967ee44ed..551256c74 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js @@ -1,3 +1,4 @@ +import { FEATURE_FLAGS } from '../../../../featureFlags'; import { frontendURL } from '../../../../helper/URLHelper'; const reports = accountId => ({ @@ -6,6 +7,7 @@ const reports = accountId => ({ 'account_overview_reports', 'conversation_reports', 'csat_reports', + 'bot_reports', 'agent_reports', 'label_reports', 'inbox_reports', @@ -33,6 +35,14 @@ const reports = accountId => ({ toState: frontendURL(`accounts/${accountId}/reports/csat`), toStateName: 'csat_reports', }, + { + icon: 'bot', + label: 'REPORTS_BOT', + hasSubMenu: false, + featureFlag: FEATURE_FLAGS.RESPONSE_BOT, + toState: frontendURL(`accounts/${accountId}/reports/bot`), + toStateName: 'bot_reports', + }, { icon: 'people', label: 'REPORTS_AGENT', diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/Location.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/Location.vue index 3b8fcbaa0..e72233b88 100644 --- a/app/javascript/dashboard/components/widgets/conversation/bubble/Location.vue +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/Location.vue @@ -1,17 +1,45 @@ + + - - - - diff --git a/app/javascript/dashboard/featureFlags.js b/app/javascript/dashboard/featureFlags.js index 2936d22ea..f2fd3e757 100644 --- a/app/javascript/dashboard/featureFlags.js +++ b/app/javascript/dashboard/featureFlags.js @@ -19,4 +19,5 @@ export const FEATURE_FLAGS = { INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply', INBOX_VIEW: 'inbox_view', SLA: 'sla', + RESPONSE_BOT: 'response_bot', }; diff --git a/app/javascript/dashboard/i18n/locale/en/contactFilters.json b/app/javascript/dashboard/i18n/locale/en/contactFilters.json index 09a543984..02d5dcf89 100644 --- a/app/javascript/dashboard/i18n/locale/en/contactFilters.json +++ b/app/javascript/dashboard/i18n/locale/en/contactFilters.json @@ -44,7 +44,8 @@ "CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox", "CREATED_AT": "Created At", "LAST_ACTIVITY": "Last Activity", - "REFERER_LINK": "Referrer link" + "REFERER_LINK": "Referrer link", + "BLOCKED": "Blocked" }, "GROUPS": { "STANDARD_FILTERS": "Standard Filters", diff --git a/app/javascript/dashboard/i18n/locale/en/report.json b/app/javascript/dashboard/i18n/locale/en/report.json index a6d476bc9..56ca21773 100644 --- a/app/javascript/dashboard/i18n/locale/en/report.json +++ b/app/javascript/dashboard/i18n/locale/en/report.json @@ -35,6 +35,14 @@ "NAME": "Resolution Count", "DESC": "( Total )" }, + "BOT_RESOLUTION_COUNT": { + "NAME": "Resolution Count", + "DESC": "( Total )" + }, + "BOT_HANDOFF_COUNT": { + "NAME": "Handoff Count", + "DESC": "( Total )" + }, "REPLY_TIME": { "NAME": "Customer waiting time", "TOOLTIP_TEXT": "Waiting time is %{metricValue} (based on %{conversationCount} replies)" @@ -86,20 +94,49 @@ "MONTH": "Month", "YEAR": "Year" }, - "GROUP_BY_DAY_OPTIONS": [{ "id": 1, "groupBy": "Day" }], + "GROUP_BY_DAY_OPTIONS": [ + { + "id": 1, + "groupBy": "Day" + } + ], "GROUP_BY_WEEK_OPTIONS": [ - { "id": 1, "groupBy": "Day" }, - { "id": 2, "groupBy": "Week" } + { + "id": 1, + "groupBy": "Day" + }, + { + "id": 2, + "groupBy": "Week" + } ], "GROUP_BY_MONTH_OPTIONS": [ - { "id": 1, "groupBy": "Day" }, - { "id": 2, "groupBy": "Week" }, - { "id": 3, "groupBy": "Month" } + { + "id": 1, + "groupBy": "Day" + }, + { + "id": 2, + "groupBy": "Week" + }, + { + "id": 3, + "groupBy": "Month" + } ], "GROUP_BY_YEAR_OPTIONS": [ - { "id": 2, "groupBy": "Week" }, - { "id": 3, "groupBy": "Month" }, - { "id": 4, "groupBy": "Year" } + { + "id": 2, + "groupBy": "Week" + }, + { + "id": 3, + "groupBy": "Month" + }, + { + "id": 4, + "groupBy": "Year" + } ], "BUSINESS_HOURS": "Business Hours" }, @@ -404,6 +441,27 @@ } } }, + "BOT_REPORTS": { + "HEADER": "Bot Reports", + "METRIC": { + "TOTAL_CONVERSATIONS": { + "LABEL": "No. of Conversations", + "TOOLTIP": "Total number of conversations handled by the bot" + }, + "TOTAL_RESPONSES": { + "LABEL": "Total Responses", + "TOOLTIP": "Total number of responses sent by the bot" + }, + "RESOLUTION_RATE": { + "LABEL": "Resolution Rate", + "TOOLTIP": "Total number of conversations resolved by the bot / Total number of conversations handled by the bot * 100" + }, + "HANDOFF_RATE": { + "LABEL": "Handoff Rate", + "TOOLTIP": "Total number of conversations handed off to agents / Total number of conversations handled by the bot * 100" + } + } + }, "OVERVIEW_REPORTS": { "HEADER": "Overview", "LIVE": "Live", diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 63725f949..9a4bde2c8 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -234,6 +234,7 @@ "CAMPAIGNS": "Campaigns", "ONGOING": "Ongoing", "ONE_OFF": "One off", + "REPORTS_BOT": "Bot", "REPORTS_AGENT": "Agents", "REPORTS_LABEL": "Labels", "REPORTS_INBOX": "Inbox", diff --git a/app/javascript/dashboard/i18n/locale/en/sla.json b/app/javascript/dashboard/i18n/locale/en/sla.json index d87f057eb..228f87066 100644 --- a/app/javascript/dashboard/i18n/locale/en/sla.json +++ b/app/javascript/dashboard/i18n/locale/en/sla.json @@ -55,11 +55,17 @@ "ERROR_MESSAGE": "There was an error, please try again" } }, - "EDIT": { - "TITLE": "Edit SLA", + "DELETE": { + "TITLE": "Delete SLA", "API": { - "SUCCESS_MESSAGE": "SLA updated successfully", + "SUCCESS_MESSAGE": "SLA deleted successfully", "ERROR_MESSAGE": "There was an error, please try again" + }, + "CONFIRM": { + "TITLE": "Confirm Deletion", + "MESSAGE": "Are you sure you want to delete ", + "YES": "Yes, Delete ", + "NO": "No, Keep " } } } diff --git a/app/javascript/dashboard/mixins/reportMixin.js b/app/javascript/dashboard/mixins/reportMixin.js index 2b8a5f87d..d57af1ad2 100644 --- a/app/javascript/dashboard/mixins/reportMixin.js +++ b/app/javascript/dashboard/mixins/reportMixin.js @@ -2,11 +2,19 @@ import { mapGetters } from 'vuex'; import { formatTime } from '@chatwoot/utils'; export default { + props: { + accountSummaryKey: { + type: String, + default: 'getAccountSummary', + }, + }, computed: { ...mapGetters({ - accountSummary: 'getAccountSummary', accountReport: 'getAccountReports', }), + accountSummary() { + return this.$store.getters[this.accountSummaryKey]; + }, }, methods: { calculateTrend(key) { diff --git a/app/javascript/dashboard/mixins/specs/reportMixin.spec.js b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js index d981de4e9..c0bc1e15f 100644 --- a/app/javascript/dashboard/mixins/specs/reportMixin.spec.js +++ b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js @@ -11,11 +11,42 @@ describe('reportMixin', () => { beforeEach(() => { getters = { getAccountSummary: () => reportFixtures.summary, + getBotSummary: () => reportFixtures.botSummary, getAccountReports: () => reportFixtures.report, }; store = new Vuex.Store({ getters }); }); + it('display the metric for account', async () => { + const Component = { + render() {}, + title: 'TestComponent', + mixins: [reportMixin], + }; + const wrapper = shallowMount(Component, { store, localVue }); + await wrapper.setProps({ + accountSummaryKey: 'getAccountSummary', + }); + expect(wrapper.vm.displayMetric('conversations_count')).toEqual('5,000'); + expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual( + '3 Min 18 Sec' + ); + }); + + it('display the metric for bot', async () => { + const Component = { + render() {}, + title: 'TestComponent', + mixins: [reportMixin], + }; + const wrapper = shallowMount(Component, { store, localVue }); + await wrapper.setProps({ + accountSummaryKey: 'getBotSummary', + }); + expect(wrapper.vm.displayMetric('bot_resolutions_count')).toEqual('10'); + expect(wrapper.vm.displayMetric('bot_handoffs_count')).toEqual('20'); + }); + it('display the metric', () => { const Component = { render() {}, diff --git a/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js b/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js index ab6b6fecf..591bf7c1f 100644 --- a/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js +++ b/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js @@ -15,6 +15,14 @@ export default { }, resolutions_count: 3, }, + botSummary: { + bot_resolutions_count: 10, + bot_handoffs_count: 20, + previous: { + bot_resolutions_count: 8, + bot_handoffs_count: 5, + }, + }, report: { data: [ { value: '0.00', timestamp: 1647541800, count: 0 }, diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue index 41ceb25e5..6f5e92017 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue @@ -243,7 +243,7 @@ export default { attr.attribute_display_type === 'checkbox' ); }); - if (isCustomAttributeCheckbox) { + if (isCustomAttributeCheckbox || type === 'blocked') { return [ { id: true, diff --git a/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js b/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js index 2d54c37bc..59376f8ef 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js +++ b/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js @@ -76,6 +76,14 @@ const filterTypes = [ filterOperators: OPERATOR_TYPES_5, attributeModel: 'standard', }, + { + attributeKey: 'blocked', + attributeI18nKey: 'BLOCKED', + inputType: 'search_select', + dataType: 'text', + filterOperators: OPERATOR_TYPES_1, + attributeModel: 'standard', + }, ]; export const filterAttributeGroups = [ @@ -115,6 +123,10 @@ export const filterAttributeGroups = [ key: 'last_activity_at', i18nKey: 'LAST_ACTIVITY', }, + { + key: 'blocked', + i18nKey: 'BLOCKED', + }, ], }, ]; diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/ChangePassword.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/ChangePassword.vue index 8eb7201ae..99cee0325 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/ChangePassword.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/ChangePassword.vue @@ -74,6 +74,7 @@ import { required, minLength } from 'vuelidate/lib/validators'; import { mapGetters } from 'vuex'; import alertMixin from 'shared/mixins/alertMixin'; +import { parseAPIErrorResponse } from 'dashboard/store/utils/api'; export default { mixins: [alertMixin], @@ -125,10 +126,9 @@ export default { }); this.errorMessage = this.$t('PROFILE_SETTINGS.PASSWORD_UPDATE_SUCCESS'); } catch (error) { - this.errorMessage = this.$t('RESET_PASSWORD.API.ERROR_MESSAGE'); - if (error?.response?.data?.message) { - this.errorMessage = error.response.data.message; - } + this.errorMessage = + parseAPIErrorResponse(error) || + this.$t('RESET_PASSWORD.API.ERROR_MESSAGE'); } finally { this.isPasswordChanging = false; this.showAlert(this.errorMessage); diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue new file mode 100644 index 000000000..a75051aaf --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue @@ -0,0 +1,106 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue index c601b4277..273f814b7 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue @@ -7,7 +7,7 @@ :key="metric.KEY" class="p-4 rounded-md mb-3" > - +
+import { ref, watch, onMounted } from 'vue'; +import ReportMetricCard from './ReportMetricCard.vue'; +import ReportsAPI from 'dashboard/api/reports'; + +const props = defineProps({ + filters: { + type: Object, + required: true, + }, +}); + +const conversationCount = ref('0'); +const messageCount = ref('0'); +const resolutionRate = ref('0'); +const handoffRate = ref('0'); + +const formatToPercent = value => { + return value ? `${value}%` : '--'; +}; + +const fetchMetrics = () => { + if (!props.filters.to || !props.filters.from) { + return; + } + ReportsAPI.getBotMetrics(props.filters).then(response => { + conversationCount.value = response.data.conversation_count.toLocaleString(); + messageCount.value = response.data.message_count.toLocaleString(); + resolutionRate.value = response.data.resolution_rate.toString(); + handoffRate.value = response.data.handoff_rate.toString(); + }); +}; + +watch(() => props.filters, fetchMetrics, { deep: true }); + +onMounted(fetchMetrics); + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ChartElements/ChartStats.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ChartElements/ChartStats.vue index 94fe1359d..4faf4de2b 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ChartElements/ChartStats.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ChartElements/ChartStats.vue @@ -1,6 +1,8 @@