Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e47d7ae894 | ||
|
|
e6df7f7dd7 | ||
|
|
053b7774dd | ||
|
|
0f5a25bad7 | ||
|
|
8eaea7c72e | ||
|
|
9f80c41e75 | ||
|
|
a4cee4f72d | ||
|
|
a34a916b46 | ||
|
|
b09a2c25fa | ||
|
|
7e65cc752e | ||
|
|
0f02c0cd5c | ||
|
|
7ade9061a8 |
+2
-2
@@ -541,9 +541,9 @@ GEM
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
maxminddb (0.1.22)
|
||||
meta_request (0.8.3)
|
||||
meta_request (0.8.5)
|
||||
rack-contrib (>= 1.1, < 3)
|
||||
railties (>= 3.0.0, < 8)
|
||||
railties (>= 3.0.0, < 9)
|
||||
method_source (1.1.0)
|
||||
mime-types (3.4.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
class V2::Reports::Conversations::MetricBuilder < V2::Reports::Conversations::BaseReportBuilder
|
||||
HIDEABLE_METRICS = %w[incoming_messages_count outgoing_messages_count reply_time].freeze
|
||||
|
||||
def summary
|
||||
{
|
||||
conversations_count: count('conversations_count'),
|
||||
incoming_messages_count: count('incoming_messages_count'),
|
||||
outgoing_messages_count: count('outgoing_messages_count'),
|
||||
incoming_messages_count: count_unless_hidden('incoming_messages_count'),
|
||||
outgoing_messages_count: count_unless_hidden('outgoing_messages_count'),
|
||||
avg_first_response_time: count('avg_first_response_time'),
|
||||
avg_resolution_time: count('avg_resolution_time'),
|
||||
resolutions_count: count('resolutions_count'),
|
||||
reply_time: count('reply_time')
|
||||
}
|
||||
reply_time: count_unless_hidden('reply_time')
|
||||
}.compact
|
||||
end
|
||||
|
||||
def bot_summary
|
||||
@@ -24,6 +26,18 @@ class V2::Reports::Conversations::MetricBuilder < V2::Reports::Conversations::Ba
|
||||
builder_class(metric).new(account, builder_params(metric)).aggregate_value
|
||||
end
|
||||
|
||||
def count_unless_hidden(metric)
|
||||
return nil if metric_hidden?(metric)
|
||||
|
||||
count(metric)
|
||||
end
|
||||
|
||||
def metric_hidden?(metric)
|
||||
return false unless HIDEABLE_METRICS.include?(metric)
|
||||
|
||||
(params[:hidden_metrics] || []).include?(metric)
|
||||
end
|
||||
|
||||
def builder_params(metric)
|
||||
params.merge({ metric: metric })
|
||||
end
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
class V2::Reports::OutgoingMessagesCountBuilder
|
||||
include DateRangeHelper
|
||||
attr_reader :account, :params
|
||||
|
||||
def initialize(account, params)
|
||||
@account = account
|
||||
@params = params
|
||||
end
|
||||
|
||||
def build
|
||||
send("build_by_#{params[:group_by]}")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_messages
|
||||
account.messages.outgoing.unscope(:order).where(created_at: range)
|
||||
end
|
||||
|
||||
def build_by_agent
|
||||
counts = base_messages
|
||||
.where(sender_type: 'User')
|
||||
.where.not(sender_id: nil)
|
||||
.group(:sender_id)
|
||||
.count
|
||||
|
||||
user_names = account.users.where(id: counts.keys).index_by(&:id)
|
||||
|
||||
counts.map do |user_id, count|
|
||||
user = user_names[user_id]
|
||||
{ id: user_id, name: user&.name, outgoing_messages_count: count }
|
||||
end
|
||||
end
|
||||
|
||||
def build_by_team
|
||||
counts = base_messages
|
||||
.joins('INNER JOIN conversations ON messages.conversation_id = conversations.id')
|
||||
.where.not(conversations: { team_id: nil })
|
||||
.group('conversations.team_id')
|
||||
.count
|
||||
|
||||
team_names = account.teams.where(id: counts.keys).index_by(&:id)
|
||||
|
||||
counts.map do |team_id, count|
|
||||
team = team_names[team_id]
|
||||
{ id: team_id, name: team&.name, outgoing_messages_count: count }
|
||||
end
|
||||
end
|
||||
|
||||
def build_by_inbox
|
||||
counts = base_messages
|
||||
.group(:inbox_id)
|
||||
.count
|
||||
|
||||
inbox_names = account.inboxes.where(id: counts.keys).index_by(&:id)
|
||||
|
||||
counts.map do |inbox_id, count|
|
||||
inbox = inbox_names[inbox_id]
|
||||
{ id: inbox_id, name: inbox&.name, outgoing_messages_count: count }
|
||||
end
|
||||
end
|
||||
|
||||
def build_by_label
|
||||
counts = base_messages
|
||||
.joins('INNER JOIN conversations ON messages.conversation_id = conversations.id')
|
||||
.joins("INNER JOIN taggings ON taggings.taggable_id = conversations.id
|
||||
AND taggings.taggable_type = 'Conversation' AND taggings.context = 'labels'")
|
||||
.joins('INNER JOIN tags ON tags.id = taggings.tag_id')
|
||||
.group('tags.name')
|
||||
.count
|
||||
|
||||
label_ids = account.labels.where(title: counts.keys).index_by(&:title)
|
||||
|
||||
counts.map do |label_name, count|
|
||||
label = label_ids[label_name]
|
||||
{ id: label&.id, name: label_name, outgoing_messages_count: count }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,10 +1,14 @@
|
||||
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
include Api::V2::Accounts::ReportsHelper
|
||||
include Api::V2::Accounts::HeatmapHelper
|
||||
include Reports::MetricFilter
|
||||
|
||||
before_action :check_authorization
|
||||
|
||||
def index
|
||||
# Return empty timeseries array for hidden metrics (preserves expected response shape)
|
||||
return render json: [] if metric_hidden?(params[:metric])
|
||||
|
||||
builder = V2::Reports::Conversations::ReportBuilder.new(Current.account, report_params)
|
||||
data = builder.timeseries
|
||||
render json: data
|
||||
@@ -78,6 +82,15 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
render json: builder.build
|
||||
end
|
||||
|
||||
OUTGOING_MESSAGES_ALLOWED_GROUP_BY = %w[agent team inbox label].freeze
|
||||
|
||||
def outgoing_messages_count
|
||||
return head :unprocessable_entity unless OUTGOING_MESSAGES_ALLOWED_GROUP_BY.include?(params[:group_by])
|
||||
|
||||
builder = V2::Reports::OutgoingMessagesCountBuilder.new(Current.account, outgoing_messages_count_params)
|
||||
render json: builder.build
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_csv(filename, template)
|
||||
@@ -103,7 +116,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
common_params.merge({
|
||||
since: range[:current][:since],
|
||||
until: range[:current][:until],
|
||||
timezone_offset: params[:timezone_offset]
|
||||
timezone_offset: params[:timezone_offset],
|
||||
hidden_metrics: hidden_metrics
|
||||
})
|
||||
end
|
||||
|
||||
@@ -111,7 +125,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
common_params.merge({
|
||||
since: range[:previous][:since],
|
||||
until: range[:previous][:until],
|
||||
timezone_offset: params[:timezone_offset]
|
||||
timezone_offset: params[:timezone_offset],
|
||||
hidden_metrics: hidden_metrics
|
||||
})
|
||||
end
|
||||
|
||||
@@ -171,4 +186,12 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
until: params[:until]
|
||||
}
|
||||
end
|
||||
|
||||
def outgoing_messages_count_params
|
||||
{
|
||||
group_by: params[:group_by],
|
||||
since: params[:since],
|
||||
until: params[:until]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
|
||||
include Reports::MetricFilter
|
||||
|
||||
before_action :check_authorization
|
||||
before_action :prepare_builder_params, only: [:agent, :team, :inbox, :label, :channel]
|
||||
|
||||
@@ -40,7 +42,7 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
|
||||
|
||||
def render_report_with(builder_class)
|
||||
builder = builder_class.new(account: Current.account, params: @builder_params)
|
||||
render json: builder.build
|
||||
render json: filter_hidden_metrics_from_array(builder.build)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
module Reports::MetricFilter
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
HIDEABLE_METRICS = %w[outgoing_messages_count incoming_messages_count reply_time].freeze
|
||||
|
||||
# Alias mapping (canonical form)
|
||||
METRIC_ALIASES = {
|
||||
'avg_reply_time' => 'reply_time'
|
||||
}.freeze
|
||||
|
||||
private
|
||||
|
||||
def filter_hidden_metrics_from_hash(data)
|
||||
return data if hidden_metrics.blank?
|
||||
|
||||
data.reject { |key, _| metric_key_hidden?(key) }
|
||||
end
|
||||
|
||||
def filter_hidden_metrics_from_array(data)
|
||||
return data if hidden_metrics.blank?
|
||||
|
||||
data.map { |item| item.reject { |key, _| metric_key_hidden?(key) } }
|
||||
end
|
||||
|
||||
# Filter summary data including nested 'previous' key (handles both symbol and string keys)
|
||||
def filter_summary_with_previous(data)
|
||||
return data if hidden_metrics.blank?
|
||||
|
||||
filtered = filter_hidden_metrics_from_hash(data)
|
||||
previous_data = filtered[:previous] || filtered['previous']
|
||||
if previous_data.present?
|
||||
filtered_previous = filter_hidden_metrics_from_hash(previous_data)
|
||||
# Preserve the original key type
|
||||
if filtered.key?(:previous)
|
||||
filtered[:previous] = filtered_previous
|
||||
else
|
||||
filtered['previous'] = filtered_previous
|
||||
end
|
||||
end
|
||||
filtered
|
||||
end
|
||||
|
||||
def hidden_metrics
|
||||
@hidden_metrics ||= (Current.account.report_hidden_metrics || []) & HIDEABLE_METRICS
|
||||
end
|
||||
|
||||
def metric_hidden?(metric_name)
|
||||
# Normalize to canonical form (e.g., avg_reply_time -> reply_time)
|
||||
normalized = METRIC_ALIASES[metric_name.to_s] || metric_name.to_s
|
||||
hidden_metrics.include?(normalized)
|
||||
end
|
||||
|
||||
# Check if a hash key should be hidden (handles both symbol and string keys, plus aliases)
|
||||
def metric_key_hidden?(key)
|
||||
key_str = key.to_s
|
||||
normalized = METRIC_ALIASES[key_str] || key_str
|
||||
hidden_metrics.include?(normalized)
|
||||
end
|
||||
end
|
||||
+3
-1
@@ -18,7 +18,9 @@ const dialogRef = ref(null);
|
||||
|
||||
const uiFlags = useMapGetter('captainResponses/getUIFlags');
|
||||
const responses = useMapGetter('captainResponses/getRecords');
|
||||
const meta = useMapGetter('captainResponses/getMeta');
|
||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||
const totalCount = computed(() => meta.value.totalCount || 0);
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close');
|
||||
@@ -37,7 +39,7 @@ defineExpose({ dialogRef });
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
type="edit"
|
||||
:title="t('CAPTAIN.DOCUMENTS.RELATED_RESPONSES.TITLE')"
|
||||
:title="`${t('CAPTAIN.DOCUMENTS.RELATED_RESPONSES.TITLE')} (${totalCount})`"
|
||||
:description="t('CAPTAIN.DOCUMENTS.RELATED_RESPONSES.DESCRIPTION')"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { computed } from 'vue';
|
||||
import { useAccount } from './useAccount';
|
||||
|
||||
// Alias mapping for consistency (canonical form is reply_time)
|
||||
const METRIC_ALIASES = {
|
||||
avg_reply_time: 'reply_time',
|
||||
};
|
||||
|
||||
export function useHiddenMetrics() {
|
||||
const { currentAccount } = useAccount();
|
||||
|
||||
const hiddenMetrics = computed(() => {
|
||||
return currentAccount.value?.settings?.report_hidden_metrics || [];
|
||||
});
|
||||
|
||||
const isMetricHidden = metricKey => {
|
||||
const normalized = METRIC_ALIASES[metricKey] || metricKey;
|
||||
return hiddenMetrics.value.includes(normalized);
|
||||
};
|
||||
|
||||
const filterVisibleReportKeys = reportKeys => {
|
||||
// Filter out hidden metrics from reportKeys object
|
||||
return Object.fromEntries(
|
||||
Object.entries(reportKeys).filter(([, value]) => !isMetricHidden(value))
|
||||
);
|
||||
};
|
||||
|
||||
return { hiddenMetrics, isMetricHidden, filterVisibleReportKeys };
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import V4Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useHiddenMetrics } from 'dashboard/composables/useHiddenMetrics';
|
||||
import ReportFilterSelector from './components/FilterSelector.vue';
|
||||
import { GROUP_BY_FILTER } from './constants';
|
||||
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
@@ -8,7 +9,7 @@ import { generateFileName } from 'dashboard/helper/downloadHelper';
|
||||
import ReportContainer from './ReportContainer.vue';
|
||||
import ReportHeader from './components/ReportHeader.vue';
|
||||
|
||||
const REPORTS_KEYS = {
|
||||
const ALL_REPORTS_KEYS = {
|
||||
CONVERSATIONS: 'conversations_count',
|
||||
INCOMING_MESSAGES: 'incoming_messages_count',
|
||||
OUTGOING_MESSAGES: 'outgoing_messages_count',
|
||||
@@ -26,6 +27,10 @@ export default {
|
||||
ReportContainer,
|
||||
V4Button,
|
||||
},
|
||||
setup() {
|
||||
const { filterVisibleReportKeys } = useHiddenMetrics();
|
||||
return { filterVisibleReportKeys };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
from: 0,
|
||||
@@ -34,6 +39,11 @@ export default {
|
||||
businessHours: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
reportKeys() {
|
||||
return this.filterVisibleReportKeys(ALL_REPORTS_KEYS);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchAllData() {
|
||||
this.fetchAccountSummary();
|
||||
@@ -47,18 +57,10 @@ export default {
|
||||
}
|
||||
},
|
||||
fetchChartData() {
|
||||
[
|
||||
'CONVERSATIONS',
|
||||
'INCOMING_MESSAGES',
|
||||
'OUTGOING_MESSAGES',
|
||||
'FIRST_RESPONSE_TIME',
|
||||
'RESOLUTION_TIME',
|
||||
'RESOLUTION_COUNT',
|
||||
'REPLY_TIME',
|
||||
].forEach(async key => {
|
||||
Object.keys(this.reportKeys).forEach(async key => {
|
||||
try {
|
||||
await this.$store.dispatch('fetchAccountReport', {
|
||||
metric: REPORTS_KEYS[key],
|
||||
metric: this.reportKeys[key],
|
||||
...this.getRequestPayload(),
|
||||
});
|
||||
} catch {
|
||||
@@ -121,6 +123,6 @@ export default {
|
||||
show-group-by-filter
|
||||
@filter-change="onFilterChange"
|
||||
/>
|
||||
<ReportContainer :group-by="groupBy" />
|
||||
<ReportContainer :group-by="groupBy" :report-keys="reportKeys" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+38
-32
@@ -2,6 +2,7 @@
|
||||
import ReportFilterSelector from './FilterSelector.vue';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useHiddenMetrics } from 'dashboard/composables/useHiddenMetrics';
|
||||
import Table from 'dashboard/components/table/Table.vue';
|
||||
import { generateFileName } from 'dashboard/helper/downloadHelper';
|
||||
import {
|
||||
@@ -44,6 +45,7 @@ import SummaryReportLink from './SummaryReportLink.vue';
|
||||
|
||||
const rowItems = useMapGetter([props.getterKey]) || [];
|
||||
const reportMetrics = useMapGetter([props.summaryKey]) || [];
|
||||
const { isMetricHidden } = useHiddenMetrics();
|
||||
|
||||
const getMetrics = id =>
|
||||
reportMetrics.value.find(metrics => metrics.id === Number(id)) || {};
|
||||
@@ -59,38 +61,42 @@ const defaulSpanRender = cellProps =>
|
||||
cellProps.getValue()
|
||||
);
|
||||
|
||||
const columns = computed(() => [
|
||||
columnHelper.accessor('name', {
|
||||
header: t(`SUMMARY_REPORTS.${props.type.toUpperCase()}`),
|
||||
width: 300,
|
||||
cell: cellProps => h(SummaryReportLink, cellProps),
|
||||
}),
|
||||
columnHelper.accessor('conversationsCount', {
|
||||
header: t('SUMMARY_REPORTS.CONVERSATIONS'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('avgFirstResponseTime', {
|
||||
header: t('SUMMARY_REPORTS.AVG_FIRST_RESPONSE_TIME'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('avgResolutionTime', {
|
||||
header: t('SUMMARY_REPORTS.AVG_RESOLUTION_TIME'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('avgReplyTime', {
|
||||
header: t('SUMMARY_REPORTS.AVG_REPLY_TIME'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('resolutionsCount', {
|
||||
header: t('SUMMARY_REPORTS.RESOLUTION_COUNT'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
]);
|
||||
const columns = computed(() => {
|
||||
const allColumns = [
|
||||
columnHelper.accessor('name', {
|
||||
header: t(`SUMMARY_REPORTS.${props.type.toUpperCase()}`),
|
||||
width: 300,
|
||||
cell: cellProps => h(SummaryReportLink, cellProps),
|
||||
}),
|
||||
columnHelper.accessor('conversationsCount', {
|
||||
header: t('SUMMARY_REPORTS.CONVERSATIONS'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('avgFirstResponseTime', {
|
||||
header: t('SUMMARY_REPORTS.AVG_FIRST_RESPONSE_TIME'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('avgResolutionTime', {
|
||||
header: t('SUMMARY_REPORTS.AVG_RESOLUTION_TIME'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
!isMetricHidden('reply_time') &&
|
||||
columnHelper.accessor('avgReplyTime', {
|
||||
header: t('SUMMARY_REPORTS.AVG_REPLY_TIME'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('resolutionsCount', {
|
||||
header: t('SUMMARY_REPORTS.RESOLUTION_COUNT'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
];
|
||||
return allColumns.filter(Boolean);
|
||||
});
|
||||
|
||||
const renderAvgTime = value => (value ? formatTime(value) : '--');
|
||||
|
||||
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import V4Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useHiddenMetrics } from 'dashboard/composables/useHiddenMetrics';
|
||||
import ReportFilters from './ReportFilters.vue';
|
||||
import ReportContainer from '../ReportContainer.vue';
|
||||
import { GROUP_BY_FILTER } from '../constants';
|
||||
@@ -63,6 +64,10 @@ export default {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { filterVisibleReportKeys } = useHiddenMetrics();
|
||||
return { filterVisibleReportKeys };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
from: 0,
|
||||
@@ -82,7 +87,7 @@ export default {
|
||||
return this.type === 'agent';
|
||||
},
|
||||
reportKeys() {
|
||||
return {
|
||||
const allKeys = {
|
||||
CONVERSATIONS: 'conversations_count',
|
||||
...(!this.isAgentType && {
|
||||
INCOMING_MESSAGES: 'incoming_messages_count',
|
||||
@@ -93,6 +98,8 @@ export default {
|
||||
RESOLUTION_COUNT: 'resolutions_count',
|
||||
REPLY_TIME: 'reply_time',
|
||||
};
|
||||
// Filter out hidden metrics
|
||||
return this.filterVisibleReportKeys(allKeys);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -67,6 +67,13 @@ class Account < ApplicationRecord
|
||||
'help_center_search': { 'type': %w[boolean null] }
|
||||
},
|
||||
'additionalProperties': false
|
||||
},
|
||||
'report_hidden_metrics': {
|
||||
'type': %w[array null],
|
||||
'items': {
|
||||
'type': 'string',
|
||||
'enum': %w[outgoing_messages_count incoming_messages_count reply_time]
|
||||
}
|
||||
}
|
||||
},
|
||||
'required': [],
|
||||
@@ -88,6 +95,7 @@ class Account < ApplicationRecord
|
||||
|
||||
store_accessor :settings, :audio_transcriptions, :auto_resolve_label
|
||||
store_accessor :settings, :captain_models, :captain_features
|
||||
store_accessor :settings, :report_hidden_metrics
|
||||
|
||||
has_many :account_users, dependent: :destroy_async
|
||||
has_many :agent_bot_inboxes, dependent: :destroy_async
|
||||
|
||||
@@ -446,6 +446,7 @@ Rails.application.routes.draw do
|
||||
get :bot_metrics
|
||||
get :inbox_label_matrix
|
||||
get :first_response_time_distribution
|
||||
get :outgoing_messages_count
|
||||
end
|
||||
end
|
||||
resource :year_in_review, only: [:show]
|
||||
|
||||
@@ -2,6 +2,9 @@ require 'administrate/field/base'
|
||||
|
||||
class AccountLimitsField < Administrate::Field::Base
|
||||
def to_s
|
||||
data.present? ? data.to_json : { agents: nil, inboxes: nil, captain_responses: nil, captain_documents: nil, emails: nil }.to_json
|
||||
defaults = { agents: nil, inboxes: nil, captain_responses: nil, captain_documents: nil, emails: nil }
|
||||
overrides = (data.presence || {}).to_h.symbolize_keys.compact
|
||||
|
||||
defaults.merge(overrides).to_json
|
||||
end
|
||||
end
|
||||
|
||||
@@ -295,4 +295,131 @@ RSpec.describe Api::V2::Accounts::ReportsController, type: :request do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/{account.id}/reports/outgoing_messages_count' do
|
||||
let(:since_epoch) { 1.week.ago.to_i.to_s }
|
||||
let(:until_epoch) { 1.day.from_now.to_i.to_s }
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'agent', since: since_epoch, until: until_epoch }
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as agent' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'agent', since: since_epoch, until: until_epoch },
|
||||
headers: agent.create_new_auth_token, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as admin' do
|
||||
let(:agent2) { create(:user, account: account, role: :agent) }
|
||||
let(:team) { create(:team, account: account) }
|
||||
let(:inbox2) { create(:inbox, account: account) }
|
||||
|
||||
# Separate conversations for agent and team grouping because
|
||||
# model callbacks clear assignee_id when team is set.
|
||||
before do
|
||||
conv_agent = create(:conversation, account: account, inbox: inbox, assignee: agent)
|
||||
conv_agent2 = create(:conversation, account: account, inbox: inbox2, assignee: agent2)
|
||||
conv_team = create(:conversation, account: account, inbox: inbox, team: team)
|
||||
|
||||
create_list(:message, 3, account: account, conversation: conv_agent, inbox: inbox, message_type: :outgoing, sender: agent)
|
||||
create_list(:message, 2, account: account, conversation: conv_agent2, inbox: inbox2, message_type: :outgoing, sender: agent2)
|
||||
create_list(:message, 4, account: account, conversation: conv_team, inbox: inbox, message_type: :outgoing)
|
||||
# incoming message should not be counted
|
||||
create(:message, account: account, conversation: conv_agent, inbox: inbox, message_type: :incoming)
|
||||
end
|
||||
|
||||
it 'returns unprocessable_entity for invalid group_by' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'invalid', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'returns outgoing message counts grouped by agent' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'agent', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data).to be_an(Array)
|
||||
|
||||
agent_entry = data.find { |e| e['id'] == agent.id }
|
||||
agent2_entry = data.find { |e| e['id'] == agent2.id }
|
||||
expect(agent_entry['outgoing_messages_count']).to eq(3)
|
||||
expect(agent2_entry['outgoing_messages_count']).to eq(2)
|
||||
end
|
||||
|
||||
it 'returns outgoing message counts grouped by team' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'team', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data).to be_an(Array)
|
||||
expect(data.length).to eq(1)
|
||||
expect(data.first['id']).to eq(team.id)
|
||||
expect(data.first['outgoing_messages_count']).to eq(4)
|
||||
end
|
||||
|
||||
it 'returns outgoing message counts grouped by inbox' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'inbox', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data).to be_an(Array)
|
||||
|
||||
inbox_entry = data.find { |e| e['id'] == inbox.id }
|
||||
inbox2_entry = data.find { |e| e['id'] == inbox2.id }
|
||||
expect(inbox_entry['outgoing_messages_count']).to eq(7)
|
||||
expect(inbox2_entry['outgoing_messages_count']).to eq(2)
|
||||
end
|
||||
|
||||
it 'returns outgoing message counts grouped by label' do
|
||||
label = create(:label, account: account, title: 'support')
|
||||
conversation = account.conversations.first
|
||||
conversation.label_list.add('support')
|
||||
conversation.save!
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'label', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data).to be_an(Array)
|
||||
expect(data.length).to eq(1)
|
||||
expect(data.first['id']).to eq(label.id)
|
||||
expect(data.first['name']).to eq('support')
|
||||
end
|
||||
|
||||
it 'excludes bot messages when grouped by agent' do
|
||||
bot = create(:agent_bot)
|
||||
bot_conversation = create(:conversation, account: account, inbox: inbox)
|
||||
create(:message, account: account, conversation: bot_conversation, inbox: inbox,
|
||||
message_type: :outgoing, sender: bot)
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'agent', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
data = response.parsed_body
|
||||
agent_entry = data.find { |e| e['id'] == agent.id }
|
||||
# 3 from before block; bot message excluded (sender_type != 'User')
|
||||
expect(agent_entry['outgoing_messages_count']).to eq(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -229,6 +229,8 @@ first_response_time_distribution:
|
||||
$ref: './resource/reports/first_response_time_distribution.yml'
|
||||
inbox_label_matrix:
|
||||
$ref: './resource/reports/inbox_label_matrix.yml'
|
||||
outgoing_messages_count:
|
||||
$ref: './resource/reports/outgoing_messages_count.yml'
|
||||
inbox_summary:
|
||||
$ref: './resource/reports/inbox_summary.yml'
|
||||
agent_summary:
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
type: array
|
||||
description: Outgoing messages count report grouped by entity (agent, team, inbox, or label).
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
description: The ID of the grouped entity (agent, team, inbox, or label).
|
||||
name:
|
||||
type: string
|
||||
description: The name of the grouped entity.
|
||||
outgoing_messages_count:
|
||||
type: number
|
||||
description: The total number of outgoing messages for this entity in the given time range.
|
||||
example:
|
||||
- id: 1
|
||||
name: Agent One
|
||||
outgoing_messages_count: 42
|
||||
- id: 2
|
||||
name: Agent Two
|
||||
outgoing_messages_count: 18
|
||||
@@ -0,0 +1,36 @@
|
||||
tags:
|
||||
- Reports
|
||||
operationId: get-outgoing-messages-count
|
||||
summary: Get outgoing messages count grouped by entity
|
||||
security:
|
||||
- userApiKey: []
|
||||
description: |
|
||||
Get the count of outgoing messages grouped by a specified entity (agent, team, inbox, or label).
|
||||
When grouped by agent, messages sent by bots (AgentBot, Captain::Assistant) are excluded.
|
||||
|
||||
**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.
|
||||
parameters:
|
||||
- in: query
|
||||
name: group_by
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- agent
|
||||
- team
|
||||
- inbox
|
||||
- label
|
||||
description: The entity to group outgoing message counts by.
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/outgoing_messages_count'
|
||||
'403':
|
||||
description: Access denied
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/bad_request_error'
|
||||
@@ -770,6 +770,23 @@
|
||||
get:
|
||||
$ref: './application/reports/inbox_label_matrix.yml'
|
||||
|
||||
# Outgoing messages count report
|
||||
/api/v2/accounts/{account_id}/reports/outgoing_messages_count:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/account_id'
|
||||
- in: query
|
||||
name: since
|
||||
schema:
|
||||
type: string
|
||||
description: The timestamp from where report should start (Unix timestamp).
|
||||
- in: query
|
||||
name: until
|
||||
schema:
|
||||
type: string
|
||||
description: The timestamp from where report should stop (Unix timestamp).
|
||||
get:
|
||||
$ref: './application/reports/outgoing_messages_count.yml'
|
||||
|
||||
# Conversations Messages
|
||||
/accounts/{account_id}/conversations/{conversation_id}/messages:
|
||||
parameters:
|
||||
|
||||
@@ -8274,6 +8274,81 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/accounts/{account_id}/reports/outgoing_messages_count": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "since",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should start (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "until",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Reports"
|
||||
],
|
||||
"operationId": "get-outgoing-messages-count",
|
||||
"summary": "Get outgoing messages count grouped by entity",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"description": "Get the count of outgoing messages grouped by a specified entity (agent, team, inbox, or label).\nWhen grouped by agent, messages sent by bots (AgentBot, Captain::Assistant) are excluded.\n\n**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.\n",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "group_by",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"agent",
|
||||
"team",
|
||||
"inbox",
|
||||
"label"
|
||||
]
|
||||
},
|
||||
"description": "The entity to group outgoing message counts by."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/outgoing_messages_count"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Access denied",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/accounts/{account_id}/conversations/{conversation_id}/messages": {
|
||||
"parameters": [
|
||||
{
|
||||
@@ -12243,6 +12318,39 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "array",
|
||||
"description": "Outgoing messages count report grouped by entity (agent, team, inbox, or label).",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The ID of the grouped entity (agent, team, inbox, or label)."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the grouped entity."
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "number",
|
||||
"description": "The total number of outgoing messages for this entity in the given time range."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Agent One",
|
||||
"outgoing_messages_count": 42
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Agent Two",
|
||||
"outgoing_messages_count": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbox_summary": {
|
||||
"type": "array",
|
||||
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
|
||||
|
||||
@@ -6816,6 +6816,81 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/accounts/{account_id}/reports/outgoing_messages_count": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "since",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should start (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "until",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Reports"
|
||||
],
|
||||
"operationId": "get-outgoing-messages-count",
|
||||
"summary": "Get outgoing messages count grouped by entity",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"description": "Get the count of outgoing messages grouped by a specified entity (agent, team, inbox, or label).\nWhen grouped by agent, messages sent by bots (AgentBot, Captain::Assistant) are excluded.\n\n**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.\n",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "group_by",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"agent",
|
||||
"team",
|
||||
"inbox",
|
||||
"label"
|
||||
]
|
||||
},
|
||||
"description": "The entity to group outgoing message counts by."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/outgoing_messages_count"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Access denied",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -10750,6 +10825,39 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "array",
|
||||
"description": "Outgoing messages count report grouped by entity (agent, team, inbox, or label).",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The ID of the grouped entity (agent, team, inbox, or label)."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the grouped entity."
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "number",
|
||||
"description": "The total number of outgoing messages for this entity in the given time range."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Agent One",
|
||||
"outgoing_messages_count": 42
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Agent Two",
|
||||
"outgoing_messages_count": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbox_summary": {
|
||||
"type": "array",
|
||||
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
|
||||
|
||||
@@ -4558,6 +4558,39 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "array",
|
||||
"description": "Outgoing messages count report grouped by entity (agent, team, inbox, or label).",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The ID of the grouped entity (agent, team, inbox, or label)."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the grouped entity."
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "number",
|
||||
"description": "The total number of outgoing messages for this entity in the given time range."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Agent One",
|
||||
"outgoing_messages_count": 42
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Agent Two",
|
||||
"outgoing_messages_count": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbox_summary": {
|
||||
"type": "array",
|
||||
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
|
||||
|
||||
@@ -3973,6 +3973,39 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "array",
|
||||
"description": "Outgoing messages count report grouped by entity (agent, team, inbox, or label).",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The ID of the grouped entity (agent, team, inbox, or label)."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the grouped entity."
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "number",
|
||||
"description": "The total number of outgoing messages for this entity in the given time range."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Agent One",
|
||||
"outgoing_messages_count": 42
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Agent Two",
|
||||
"outgoing_messages_count": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbox_summary": {
|
||||
"type": "array",
|
||||
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
|
||||
|
||||
@@ -4734,6 +4734,39 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "array",
|
||||
"description": "Outgoing messages count report grouped by entity (agent, team, inbox, or label).",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The ID of the grouped entity (agent, team, inbox, or label)."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the grouped entity."
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "number",
|
||||
"description": "The total number of outgoing messages for this entity in the given time range."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Agent One",
|
||||
"outgoing_messages_count": 42
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Agent Two",
|
||||
"outgoing_messages_count": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbox_summary": {
|
||||
"type": "array",
|
||||
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
|
||||
|
||||
Reference in New Issue
Block a user