+
{{ label }}
checkPermissions(['administrator']));
const selectedRange = ref('this_month');
@@ -69,34 +74,38 @@ const metricFor = (statKey, formatValue, direction, trendKind = 'percent') => {
const metrics = computed(() => [
{
key: 'handled',
+ metric: 'conversations_handled',
label: t('CAPTAIN.OVERVIEW.METRICS.HANDLED.LABEL'),
hint: t('CAPTAIN.OVERVIEW.METRICS.HANDLED.HINT'),
...metricFor('conversations_handled', v => v.toLocaleString(), 'up'),
},
{
key: 'autoResolution',
+ metric: 'auto_resolution_rate',
label: t('CAPTAIN.OVERVIEW.METRICS.AUTO_RESOLUTION.LABEL'),
hint: t('CAPTAIN.OVERVIEW.METRICS.AUTO_RESOLUTION.HINT'),
...metricFor('auto_resolution_rate', v => `${v}%`, 'up', 'point'),
},
{
key: 'handoff',
+ metric: 'handoff_rate',
label: t('CAPTAIN.OVERVIEW.METRICS.HANDOFF.LABEL'),
hint: t('CAPTAIN.OVERVIEW.METRICS.HANDOFF.HINT'),
...metricFor('handoff_rate', v => `${v}%`, 'down', 'point'),
},
+ {
+ key: 'reopen',
+ metric: 'reopen_rate',
+ label: t('CAPTAIN.OVERVIEW.METRICS.REOPEN.LABEL'),
+ hint: t('CAPTAIN.OVERVIEW.METRICS.REOPEN.HINT'),
+ ...metricFor('reopen_rate', v => `${v}%`, 'down', 'point'),
+ },
{
key: 'hoursSaved',
label: t('CAPTAIN.OVERVIEW.METRICS.HOURS_SAVED.LABEL'),
hint: t('CAPTAIN.OVERVIEW.METRICS.HOURS_SAVED.HINT'),
...metricFor('hours_saved', formatDuration, 'up'),
},
- {
- key: 'reopen',
- label: t('CAPTAIN.OVERVIEW.METRICS.REOPEN.LABEL'),
- hint: t('CAPTAIN.OVERVIEW.METRICS.REOPEN.HINT'),
- ...metricFor('reopen_rate', v => `${v}%`, 'down', 'point'),
- },
{
key: 'depth',
label: t('CAPTAIN.OVERVIEW.METRICS.DEPTH.LABEL'),
@@ -109,6 +118,22 @@ const metrics = computed(() => [
),
},
]);
+
+const drilldown = ref({ metric: '', label: '', value: '' });
+const isDrilldownOpen = ref(false);
+
+const openDrilldown = metric => {
+ drilldown.value = {
+ metric: metric.metric,
+ label: metric.label,
+ value: metric.value,
+ };
+ isDrilldownOpen.value = true;
+};
+
+const closeDrilldown = () => {
+ isDrilldownOpen.value = false;
+};
@@ -144,6 +169,8 @@ const metrics = computed(() => [
:trend="metric.trend"
:hint="metric.hint"
:trend-good="metric.trendGood"
+ :clickable="canDrilldown && Boolean(metric.metric)"
+ @click="openDrilldown(metric)"
/>
@@ -151,6 +178,17 @@ const metrics = computed(() => [
+
+
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