diff --git a/app/javascript/dashboard/api/captain/assistant.js b/app/javascript/dashboard/api/captain/assistant.js index dcd92f735..1fc17798d 100644 --- a/app/javascript/dashboard/api/captain/assistant.js +++ b/app/javascript/dashboard/api/captain/assistant.js @@ -37,6 +37,20 @@ class CaptainAssistant extends ApiClient { params: { range, timezone_offset: getTimezoneOffset() }, }); } + + getDrilldown({ assistantId, metric, range, page, signal }) { + const requestConfig = { + params: { + metric, + range, + timezone_offset: getTimezoneOffset(), + page, + }, + }; + if (signal) requestConfig.signal = signal; + + return axios.get(`${this.url}/${assistantId}/drilldown`, requestConfig); + } } export default new CaptainAssistant(); diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/overview/AssistantDrilldownDrawer.vue b/app/javascript/dashboard/components-next/captain/pageComponents/overview/AssistantDrilldownDrawer.vue new file mode 100644 index 000000000..cf17e7804 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/pageComponents/overview/AssistantDrilldownDrawer.vue @@ -0,0 +1,234 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/overview/MetricCard.vue b/app/javascript/dashboard/components-next/captain/pageComponents/overview/MetricCard.vue index 9f8a76f43..cf66a0a2f 100644 --- a/app/javascript/dashboard/components-next/captain/pageComponents/overview/MetricCard.vue +++ b/app/javascript/dashboard/components-next/captain/pageComponents/overview/MetricCard.vue @@ -8,16 +8,35 @@ const props = defineProps({ hint: { type: String, default: '' }, // null = neutral, true = good direction, false = bad direction trendGood: { type: Boolean, default: null }, + clickable: { type: Boolean, default: false }, }); +const emit = defineEmits(['click']); + const trendClass = computed(() => { if (props.trendGood === null) return 'text-n-slate-11'; return props.trendGood ? 'text-n-teal-11' : 'text-n-ruby-11'; }); + +const onActivate = () => { + if (props.clickable) emit('click'); +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/composables/useReportDrilldown.js b/app/javascript/dashboard/routes/dashboard/settings/reports/composables/useReportDrilldown.js index 7c37cd9cc..8309ed360 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/composables/useReportDrilldown.js +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/composables/useReportDrilldown.js @@ -1,7 +1,13 @@ import { computed, ref } from 'vue'; import ReportsAPI from 'dashboard/api/reports'; -export function useReportDrilldown() { +// `fetcher` is any `({ ...request, page, signal }) => Promise` returning the +// shared drilldown envelope (`{ data: { meta, payload } }`), so the same paging +// and abort machinery backs both the reports and Captain assistant drilldowns. +// The default is wrapped so `ReportsAPI` stays the receiver when invoked. +export function useReportDrilldown( + fetcher = params => ReportsAPI.getDrilldown(params) +) { const activeRequest = ref(null); const records = ref([]); const meta = ref({}); @@ -20,17 +26,7 @@ export function useReportDrilldown() { const isCurrentRequest = token => token === requestToken && !!activeRequest.value; - const requestFingerprint = request => - JSON.stringify({ - metric: request.metric, - bucketTimestamp: request.bucketTimestamp, - from: request.from, - to: request.to, - type: request.type, - id: request.id, - groupBy: request.groupBy, - businessHours: request.businessHours, - }); + const requestFingerprint = request => JSON.stringify(request); const abortActiveRequest = () => { if (!activeRequestController) return; @@ -55,7 +51,7 @@ export function useReportDrilldown() { hasError.value = false; try { - const response = await ReportsAPI.getDrilldown({ + const response = await fetcher({ ...request, page, signal: controller.signal, diff --git a/enterprise/app/builders/captain/assistant_drilldown_builder.rb b/enterprise/app/builders/captain/assistant_drilldown_builder.rb index b98aa7620..fb5d3a7e8 100644 --- a/enterprise/app/builders/captain/assistant_drilldown_builder.rb +++ b/enterprise/app/builders/captain/assistant_drilldown_builder.rb @@ -1,6 +1,6 @@ # Lists the underlying records behind a single Captain assistant stat card, so a # viewer can drill from an aggregate (e.g. "auto-resolution 42%") into the exact -# conversations or messages that produced it. +# conversations that produced it. # # The window is resolved by Captain::AssistantStatsWindow from the same `range` # and `timezone_offset` the stat card used, so the drilldown covers precisely the @@ -11,10 +11,8 @@ class Captain::AssistantDrilldownBuilder RESOLVED_EVENT_NAMES = Captain::AssistantStatsBuilder::RESOLVED_EVENT_NAMES HANDOFF_EVENT_NAMES = Captain::AssistantStatsBuilder::HANDOFF_EVENT_NAMES - # Metrics whose records are individual messages rather than conversations. - MESSAGE_METRICS = %w[hours_saved].freeze SUPPORTED_METRICS = %w[ - conversations_handled auto_resolution_rate handoff_rate hours_saved reopen_rate conversation_depth + conversations_handled auto_resolution_rate handoff_rate reopen_rate ].freeze DEFAULT_PAGE = 1 @@ -43,21 +41,14 @@ class Captain::AssistantDrilldownBuilder def meta { metric: metric, - record_type: record_type, current_page: current_page, per_page: per_page, total_count: paginated_records.total_count, - conversation_count: conversation_count, + conversation_count: paginated_records.total_count, range: { since: range.first.to_i, until: range.last.to_i } } end - def conversation_count - return paginated_records.total_count unless message_metric? - - drilldown_scope.except(:includes).reorder(nil).distinct.count(:conversation_id) - end - def paginated_records @paginated_records ||= drilldown_scope.page(current_page).per(per_page) end @@ -67,9 +58,7 @@ class Captain::AssistantDrilldownBuilder when 'conversations_handled' then handled_conversations when 'auto_resolution_rate' then conversations_for(resolved_events.select(:conversation_id)) when 'handoff_rate' then event_conversations(HANDOFF_EVENT_NAMES) - when 'hours_saved' then public_reply_messages when 'reopen_rate' then reopened_conversations - when 'conversation_depth' then depth_conversations else raise ArgumentError, "Unsupported assistant drilldown metric: #{metric}" end @@ -88,13 +77,6 @@ class Captain::AssistantDrilldownBuilder conversations_for(handled_conversation_ids) end - # Public agent-facing replies the assistant sent; the rows behind hours_saved. - def public_reply_messages - handled_messages.where(message_type: :outgoing, private: false) - .includes(:sender, conversation: [:assignee, :contact, :inbox]) - .reorder(created_at: :desc) - end - # Conversations in the handled cohort that recorded one of the given reporting # events in the window (resolved or handed-off). def event_conversations(event_names) @@ -129,11 +111,6 @@ class Captain::AssistantDrilldownBuilder conversations_for(ids) end - # Conversations the assistant sent at least one public reply in; the denominator behind conversation_depth. - def depth_conversations - conversations_for(handled_messages.where(message_type: :outgoing, private: false).select(:conversation_id)) - end - def conversations_for(conversation_ids) account.conversations .where(id: conversation_ids) @@ -147,10 +124,6 @@ class Captain::AssistantDrilldownBuilder def metric = params[:metric].to_s - def message_metric? = MESSAGE_METRICS.include?(metric) - - def record_type = message_metric? ? 'message' : 'conversation' - def current_page = [params[:page].to_i, DEFAULT_PAGE].max def per_page