diff --git a/app/builders/v2/reports/label_summary_builder.rb b/app/builders/v2/reports/label_summary_builder.rb index caa5a04d8..8b7c21e8e 100644 --- a/app/builders/v2/reports/label_summary_builder.rb +++ b/app/builders/v2/reports/label_summary_builder.rb @@ -28,7 +28,7 @@ class V2::Reports::LabelSummaryBuilder < V2::Reports::BaseSummaryBuilder { conversation_counts: fetch_conversation_counts(conversation_filter), - resolved_counts: fetch_resolved_counts(conversation_filter), + resolved_counts: fetch_resolved_counts, resolution_metrics: fetch_metrics(conversation_filter, 'conversation_resolved', use_business_hours), first_response_metrics: fetch_metrics(conversation_filter, 'first_response', use_business_hours), reply_metrics: fetch_metrics(conversation_filter, 'reply_time', use_business_hours) @@ -62,10 +62,21 @@ class V2::Reports::LabelSummaryBuilder < V2::Reports::BaseSummaryBuilder fetch_counts(conversation_filter) end - def fetch_resolved_counts(conversation_filter) - # since the base query is ActsAsTaggableOn, - # the status :resolved won't automatically be converted to integer status - fetch_counts(conversation_filter.merge(status: Conversation.statuses[:resolved])) + def fetch_resolved_counts + # Count resolution events, not conversations currently in resolved status + # Filter by reporting_event.created_at, not conversation.created_at + reporting_event_filter = { name: 'conversation_resolved', account_id: account.id } + reporting_event_filter[:created_at] = range if range.present? + + ReportingEvent + .joins(conversation: { taggings: :tag }) + .where( + reporting_event_filter.merge( + taggings: { taggable_type: 'Conversation', context: 'labels' } + ) + ) + .group('tags.name') + .count end def fetch_counts(conversation_filter) @@ -84,9 +95,7 @@ class V2::Reports::LabelSummaryBuilder < V2::Reports::BaseSummaryBuilder def fetch_metrics(conversation_filter, event_name, use_business_hours) ReportingEvent - .joins('INNER JOIN conversations ON reporting_events.conversation_id = conversations.id') - .joins('INNER JOIN taggings ON taggings.taggable_id = conversations.id') - .joins('INNER JOIN tags ON taggings.tag_id = tags.id') + .joins(conversation: { taggings: :tag }) .where( conversations: conversation_filter, name: event_name, diff --git a/app/builders/v2/reports/timeseries/count_report_builder.rb b/app/builders/v2/reports/timeseries/count_report_builder.rb index 03a87a6fa..bb3b1250c 100644 --- a/app/builders/v2/reports/timeseries/count_report_builder.rb +++ b/app/builders/v2/reports/timeseries/count_report_builder.rb @@ -38,27 +38,34 @@ class V2::Reports::Timeseries::CountReportBuilder < V2::Reports::Timeseries::Bas end def scope_for_resolutions_count - scope.reporting_events.joins(:conversation).select(:conversation_id).where( + scope.reporting_events.where( name: :conversation_resolved, - conversations: { status: :resolved }, created_at: range - ).distinct + account_id: account.id, + created_at: range + ) end def scope_for_bot_resolutions_count - scope.reporting_events.joins(:conversation).select(:conversation_id).where( + scope.reporting_events.where( name: :conversation_bot_resolved, - conversations: { status: :resolved }, created_at: range - ).distinct + account_id: account.id, + created_at: range + ) end def scope_for_bot_handoffs_count scope.reporting_events.joins(:conversation).select(:conversation_id).where( name: :conversation_bot_handoff, + account_id: account.id, created_at: range ).distinct end def grouped_count + # IMPORTANT: time_zone parameter affects both data grouping AND output timestamps + # It converts timestamps to the target timezone before grouping, which means + # the same event can fall into different day buckets depending on timezone + # Example: 2024-01-15 00:00 UTC becomes 2024-01-14 16:00 PST (falls on different day) @grouped_values = object_scope.group_by_period( group_by, :created_at, diff --git a/app/controllers/api/v1/widget/configs_controller.rb b/app/controllers/api/v1/widget/configs_controller.rb index ac88c595a..ecbddd905 100644 --- a/app/controllers/api/v1/widget/configs_controller.rb +++ b/app/controllers/api/v1/widget/configs_controller.rb @@ -9,7 +9,7 @@ class Api::V1::Widget::ConfigsController < Api::V1::Widget::BaseController private def set_global_config - @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL') + @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'INSTALLATION_NAME') end def set_contact diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 4a2df5ee5..d81b4c9da 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -66,7 +66,7 @@ class DashboardController < ActionController::Base ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'), FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''), INSTAGRAM_APP_ID: GlobalConfigService.load('INSTAGRAM_APP_ID', ''), - FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'), + FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v18.0'), WHATSAPP_APP_ID: GlobalConfigService.load('WHATSAPP_APP_ID', ''), WHATSAPP_CONFIGURATION_ID: GlobalConfigService.load('WHATSAPP_CONFIGURATION_ID', ''), IS_ENTERPRISE: ChatwootApp.enterprise?, diff --git a/app/controllers/public/api/v1/portals/base_controller.rb b/app/controllers/public/api/v1/portals/base_controller.rb index 66b052b1e..46158bce9 100644 --- a/app/controllers/public/api/v1/portals/base_controller.rb +++ b/app/controllers/public/api/v1/portals/base_controller.rb @@ -58,6 +58,6 @@ class Public::Api::V1::Portals::BaseController < PublicController end def set_global_config - @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'BRAND_URL') + @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'BRAND_URL', 'INSTALLATION_NAME') end end diff --git a/app/controllers/survey/responses_controller.rb b/app/controllers/survey/responses_controller.rb index 8bbd0fe88..afcb3f4c0 100644 --- a/app/controllers/survey/responses_controller.rb +++ b/app/controllers/survey/responses_controller.rb @@ -5,6 +5,6 @@ class Survey::ResponsesController < ActionController::Base private def set_global_config - @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL') + @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'INSTALLATION_NAME') end end diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb index 70e4c967b..4be690ffe 100644 --- a/app/controllers/widgets_controller.rb +++ b/app/controllers/widgets_controller.rb @@ -14,7 +14,7 @@ class WidgetsController < ActionController::Base private def set_global_config - @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'DIRECT_UPLOADS_ENABLED') + @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'DIRECT_UPLOADS_ENABLED', 'INSTALLATION_NAME') end def set_web_widget diff --git a/app/helpers/report_helper.rb b/app/helpers/report_helper.rb index 99f3fd36b..09a84b110 100644 --- a/app/helpers/report_helper.rb +++ b/app/helpers/report_helper.rb @@ -53,13 +53,13 @@ module ReportHelper 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 + scope.reporting_events.where(account_id: account.id, name: :conversation_resolved, + created_at: range) end def bot_resolutions - scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_resolved, - conversations: { status: :resolved }, created_at: range).distinct + scope.reporting_events.where(account_id: account.id, name: :conversation_bot_resolved, + created_at: range) end def bot_handoffs diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentCard/AssignmentCard.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentCard/AssignmentCard.story.vue new file mode 100644 index 000000000..e35be8155 --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentCard/AssignmentCard.story.vue @@ -0,0 +1,63 @@ + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentCard/AssignmentCard.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentCard/AssignmentCard.vue new file mode 100644 index 000000000..1e477eafe --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentCard/AssignmentCard.vue @@ -0,0 +1,49 @@ + + + diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue index ea5c63ebc..0b4880264 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue @@ -62,6 +62,7 @@ const segmentsQuery = ref({}); const appliedFilters = useMapGetter('contacts/getAppliedContactFiltersV4'); const contactAttributes = useMapGetter('attributes/getContactAttributes'); +const labels = useMapGetter('labels/getLabels'); const hasActiveSegments = computed( () => props.activeSegment && props.segmentsId !== 0 ); @@ -215,6 +216,7 @@ const setParamsForEditSegmentModal = () => { countries, filterTypes: contactFilterItems, allCustomAttributes: useSnakeCase(contactAttributes.value), + labels: labels.value || [], }; }; diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue index d6e546581..bb5f03451 100644 --- a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue +++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue @@ -4,6 +4,10 @@ import { useToggle } from '@vueuse/core'; import { useI18n } from 'vue-i18n'; import { dynamicTime } from 'shared/helpers/timeHelper'; import { usePolicy } from 'dashboard/composables/usePolicy'; +import { + isPdfDocument, + formatDocumentLink, +} from 'shared/helpers/documentHelper'; import CardLayout from 'dashboard/components-next/CardLayout.vue'; import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue'; @@ -63,6 +67,11 @@ const menuItems = computed(() => { const createdAt = computed(() => dynamicTime(props.createdAt)); +const displayLink = computed(() => formatDocumentLink(props.externalLink)); +const linkIcon = computed(() => + isPdfDocument(props.externalLink) ? 'i-ph-file-pdf' : 'i-ph-link-simple' +); + const handleAction = ({ action, value }) => { toggleDropdown(false); emit('action', { action, value, id: props.id }); @@ -71,14 +80,14 @@ const handleAction = ({ action, value }) => { diff --git a/app/javascript/dashboard/components-next/message/constants.js b/app/javascript/dashboard/components-next/message/constants.js index 71257f66a..77ea25533 100644 --- a/app/javascript/dashboard/components-next/message/constants.js +++ b/app/javascript/dashboard/components-next/message/constants.js @@ -64,6 +64,7 @@ export const CONTENT_TYPES = { INPUT_CSAT: 'input_csat', INTEGRATIONS: 'integrations', STICKER: 'sticker', + VOICE_CALL: 'voice_call', }; export const MEDIA_TYPES = [ diff --git a/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue b/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue index 2a93089a9..db0e08d04 100644 --- a/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue +++ b/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue @@ -25,7 +25,7 @@ const reauthorizationRequired = computed(() => {