From f1f1ce644c5262e37266f5d506b06659946bb13f Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 13 Oct 2025 19:15:57 +0530 Subject: [PATCH] feat: Overview heatmap improvements (#12359) This PR adds inbox filtering to the conversation traffic heatmap, allowing users to analyze patterns for specific inboxes. Additionally, it also adds a new resolution count heatmap that shows when support teams are most active in resolving conversations, using a green color to distinguish it from the blue conversation heatmap. The PR also reorganizes heatmap components into a cleaner structure with a shared `BaseHeatmapContainer` that handles common functionality like date range selection, inbox filtering, and data fetching. This makes it easy to add new heatmap metrics in the future - just create a wrapper component specifying the metric type and color scheme. CleanShot 2025-10-13 at 14 01
35@2x CleanShot 2025-10-13 at 14 03
00@2x Unrelated change, the data seeder conversation resolution would not work correctly, we've fixed it. --------- Co-authored-by: Muhsin Keloth --- .../dashboard/i18n/locale/en/report.json | 10 + .../settings/reports/LiveReports.vue | 6 +- .../settings/reports/components/Heatmap.vue | 175 ------------ .../reports/components/HeatmapContainer.vue | 119 -------- .../components/heatmaps/BaseHeatmap.vue | 214 ++++++++++++++ .../heatmaps/BaseHeatmapContainer.vue | 265 ++++++++++++++++++ .../heatmaps/ConversationHeatmapContainer.vue | 18 ++ .../components/heatmaps/HeatmapTooltip.vue | 57 ++++ .../heatmaps/ResolutionHeatmapContainer.vue | 18 ++ .../heatmaps/composables/useHeatmapTooltip.js | 34 +++ .../dashboard/store/modules/reports.js | 21 ++ .../dashboard/store/mutation-types.js | 2 + lib/seeders/reports/conversation_creator.rb | 36 ++- 13 files changed, 668 insertions(+), 307 deletions(-) delete mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/Heatmap.vue delete mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/HeatmapContainer.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/BaseHeatmap.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/BaseHeatmapContainer.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/ConversationHeatmapContainer.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/HeatmapTooltip.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/ResolutionHeatmapContainer.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/composables/useHeatmapTooltip.js diff --git a/app/javascript/dashboard/i18n/locale/en/report.json b/app/javascript/dashboard/i18n/locale/en/report.json index 7c42fdfba..c622170b0 100644 --- a/app/javascript/dashboard/i18n/locale/en/report.json +++ b/app/javascript/dashboard/i18n/locale/en/report.json @@ -51,6 +51,7 @@ }, "DATE_RANGE_OPTIONS": { "LAST_7_DAYS": "Last 7 days", + "LAST_14_DAYS": "Last 14 days", "LAST_30_DAYS": "Last 30 days", "LAST_3_MONTHS": "Last 3 months", "LAST_6_MONTHS": "Last 6 months", @@ -266,6 +267,8 @@ "NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.", "DOWNLOAD_INBOX_REPORTS": "Download inbox reports", "FILTER_DROPDOWN_LABEL": "Select Inbox", + "ALL_INBOXES": "All Inboxes", + "SEARCH_INBOX": "Search Inbox", "METRICS": { "CONVERSATIONS": { "NAME": "Conversations", @@ -467,6 +470,13 @@ "CONVERSATIONS": "{count} conversations", "DOWNLOAD_REPORT": "Download report" }, + "RESOLUTION_HEATMAP": { + "HEADER": "Resolutions", + "NO_CONVERSATIONS": "No conversations", + "CONVERSATION": "{count} conversation", + "CONVERSATIONS": "{count} conversations", + "DOWNLOAD_REPORT": "Download report" + }, "AGENT_CONVERSATIONS": { "HEADER": "Conversations by agents", "LOADING_MESSAGE": "Loading agent metrics...", diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/LiveReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/LiveReports.vue index a0eeb3ab9..1eb9640ac 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/LiveReports.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/LiveReports.vue @@ -1,6 +1,7 @@ - - diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/HeatmapContainer.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/HeatmapContainer.vue deleted file mode 100644 index 280e1d6ee..000000000 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/HeatmapContainer.vue +++ /dev/null @@ -1,119 +0,0 @@ - - - diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/BaseHeatmap.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/BaseHeatmap.vue new file mode 100644 index 000000000..3f9dd9db4 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/BaseHeatmap.vue @@ -0,0 +1,214 @@ + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/BaseHeatmapContainer.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/BaseHeatmapContainer.vue new file mode 100644 index 000000000..2a692b12a --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/BaseHeatmapContainer.vue @@ -0,0 +1,265 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/ConversationHeatmapContainer.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/ConversationHeatmapContainer.vue new file mode 100644 index 000000000..394b7cdc2 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/ConversationHeatmapContainer.vue @@ -0,0 +1,18 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/HeatmapTooltip.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/HeatmapTooltip.vue new file mode 100644 index 000000000..79377a6a3 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/HeatmapTooltip.vue @@ -0,0 +1,57 @@ + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/ResolutionHeatmapContainer.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/ResolutionHeatmapContainer.vue new file mode 100644 index 000000000..24530d0c7 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/ResolutionHeatmapContainer.vue @@ -0,0 +1,18 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/composables/useHeatmapTooltip.js b/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/composables/useHeatmapTooltip.js new file mode 100644 index 000000000..28b050542 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/heatmaps/composables/useHeatmapTooltip.js @@ -0,0 +1,34 @@ +import { ref } from 'vue'; + +export function useHeatmapTooltip() { + const visible = ref(false); + const x = ref(0); + const y = ref(0); + const value = ref(null); + + let timeoutId = null; + + const show = (event, cellValue) => { + clearTimeout(timeoutId); + + // Update position immediately for smooth movement + const rect = event.target.getBoundingClientRect(); + x.value = rect.left + rect.width / 2; + y.value = rect.top; + + // Only delay content update and visibility + timeoutId = setTimeout(() => { + value.value = cellValue; + visible.value = true; + }, 100); + }; + + const hide = () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + visible.value = false; + }, 50); + }; + + return { visible, x, y, value, show, hide }; +} diff --git a/app/javascript/dashboard/store/modules/reports.js b/app/javascript/dashboard/store/modules/reports.js index bb5364bb5..99b2acf18 100644 --- a/app/javascript/dashboard/store/modules/reports.js +++ b/app/javascript/dashboard/store/modules/reports.js @@ -57,11 +57,13 @@ const state = { uiFlags: { isFetchingAccountConversationMetric: false, isFetchingAccountConversationsHeatmap: false, + isFetchingAccountResolutionsHeatmap: false, isFetchingAgentConversationMetric: false, isFetchingTeamConversationMetric: false, }, accountConversationMetric: {}, accountConversationHeatmap: [], + accountResolutionHeatmap: [], agentConversationMetric: [], teamConversationMetric: [], }, @@ -89,6 +91,9 @@ const getters = { getAccountConversationHeatmapData(_state) { return _state.overview.accountConversationHeatmap; }, + getAccountResolutionHeatmapData(_state) { + return _state.overview.accountResolutionHeatmap; + }, getAgentConversationMetric(_state) { return _state.overview.agentConversationMetric; }, @@ -130,6 +135,16 @@ export const actions = { commit(types.default.TOGGLE_HEATMAP_LOADING, false); }); }, + fetchAccountResolutionHeatmap({ commit }, reportObj) { + commit(types.default.TOGGLE_RESOLUTION_HEATMAP_LOADING, true); + Report.getReports({ ...reportObj, groupBy: 'hour' }).then(heatmapData => { + let { data } = heatmapData; + data = clampDataBetweenTimeline(data, reportObj.from, reportObj.to); + + commit(types.default.SET_RESOLUTION_HEATMAP_DATA, data); + commit(types.default.TOGGLE_RESOLUTION_HEATMAP_LOADING, false); + }); + }, fetchAccountSummary({ commit }, reportObj) { commit(types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FETCHING); Report.getSummary( @@ -287,6 +302,9 @@ const mutations = { [types.default.SET_HEATMAP_DATA](_state, heatmapData) { _state.overview.accountConversationHeatmap = heatmapData; }, + [types.default.SET_RESOLUTION_HEATMAP_DATA](_state, heatmapData) { + _state.overview.accountResolutionHeatmap = heatmapData; + }, [types.default.TOGGLE_ACCOUNT_REPORT_LOADING](_state, { metric, value }) { _state.accountReport.isFetching[metric] = value; }, @@ -299,6 +317,9 @@ const mutations = { [types.default.TOGGLE_HEATMAP_LOADING](_state, flag) { _state.overview.uiFlags.isFetchingAccountConversationsHeatmap = flag; }, + [types.default.TOGGLE_RESOLUTION_HEATMAP_LOADING](_state, flag) { + _state.overview.uiFlags.isFetchingAccountResolutionsHeatmap = flag; + }, [types.default.SET_ACCOUNT_SUMMARY](_state, summaryData) { _state.accountSummary = summaryData; }, diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 4f361e140..68ff79e66 100644 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -187,6 +187,8 @@ export default { SET_ACCOUNT_REPORTS: 'SET_ACCOUNT_REPORTS', SET_HEATMAP_DATA: 'SET_HEATMAP_DATA', TOGGLE_HEATMAP_LOADING: 'TOGGLE_HEATMAP_LOADING', + SET_RESOLUTION_HEATMAP_DATA: 'SET_RESOLUTION_HEATMAP_DATA', + TOGGLE_RESOLUTION_HEATMAP_LOADING: 'TOGGLE_RESOLUTION_HEATMAP_LOADING', SET_ACCOUNT_SUMMARY: 'SET_ACCOUNT_SUMMARY', SET_BOT_SUMMARY: 'SET_BOT_SUMMARY', TOGGLE_ACCOUNT_REPORT_LOADING: 'TOGGLE_ACCOUNT_REPORT_LOADING', diff --git a/lib/seeders/reports/conversation_creator.rb b/lib/seeders/reports/conversation_creator.rb index b6259de7d..1cd11ef33 100644 --- a/lib/seeders/reports/conversation_creator.rb +++ b/lib/seeders/reports/conversation_creator.rb @@ -16,8 +16,11 @@ class Seeders::Reports::ConversationCreator @priorities = [nil, 'urgent', 'high', 'medium', 'low'] end + # rubocop:disable Metrics/MethodLength def create_conversation(created_at:) conversation = nil + should_resolve = false + resolution_time = nil ActiveRecord::Base.transaction do travel_to(created_at) do @@ -26,14 +29,35 @@ class Seeders::Reports::ConversationCreator add_labels_to_conversation(conversation) create_messages_for_conversation(conversation) - resolve_conversation_if_needed(conversation) + + # Determine if should resolve but don't update yet + should_resolve = rand > 0.3 + if should_resolve + resolution_delay = rand((30.minutes)..(24.hours)) + resolution_time = created_at + resolution_delay + end end travel_back end + # Now resolve outside of time travel if needed + if should_resolve && resolution_time + # rubocop:disable Rails/SkipsModelValidations + conversation.update_column(:status, :resolved) + conversation.update_column(:updated_at, resolution_time) + # rubocop:enable Rails/SkipsModelValidations + + # Trigger the event with proper timestamp + travel_to(resolution_time) do + trigger_conversation_resolved_event(conversation) + end + travel_back + end + conversation end + # rubocop:enable Metrics/MethodLength private @@ -85,16 +109,6 @@ class Seeders::Reports::ConversationCreator message_creator.create_messages end - def resolve_conversation_if_needed(conversation) - return unless rand < 0.7 - - resolution_delay = rand((30.minutes)..(24.hours)) - travel(resolution_delay) - conversation.update!(status: :resolved) - - trigger_conversation_resolved_event(conversation) - end - def trigger_conversation_resolved_event(conversation) event_data = { conversation: conversation }