feat: Add the bot performance reports UI (#9036)
Co-authored-by: Pranav <pranav@chatwoot.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto p-4">
|
||||
<report-filter-selector
|
||||
:show-agents-filter="false"
|
||||
:show-group-by-filter="true"
|
||||
:show-business-hours-switch="false"
|
||||
@filter-change="onFilterChange"
|
||||
/>
|
||||
|
||||
<bot-metrics :filters="requestPayload" />
|
||||
<report-container
|
||||
:group-by="groupBy"
|
||||
:report-keys="reportKeys"
|
||||
:account-summary-key="'getBotSummary'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import BotMetrics from './components/BotMetrics.vue';
|
||||
import ReportFilterSelector from './components/FilterSelector.vue';
|
||||
import { GROUP_BY_FILTER } from './constants';
|
||||
import reportMixin from 'dashboard/mixins/reportMixin';
|
||||
import ReportContainer from './ReportContainer.vue';
|
||||
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
name: 'BotReports',
|
||||
components: {
|
||||
BotMetrics,
|
||||
ReportFilterSelector,
|
||||
ReportContainer,
|
||||
},
|
||||
mixins: [reportMixin],
|
||||
data() {
|
||||
return {
|
||||
from: 0,
|
||||
to: 0,
|
||||
groupBy: GROUP_BY_FILTER[1],
|
||||
reportKeys: {
|
||||
BOT_RESOLUTION_COUNT: 'bot_resolutions_count',
|
||||
BOT_HANDOFF_COUNT: 'bot_handoffs_count',
|
||||
},
|
||||
businessHours: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountReport: 'getAccountReports',
|
||||
}),
|
||||
requestPayload() {
|
||||
return {
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchAllData() {
|
||||
this.fetchBotSummary();
|
||||
this.fetchChartData();
|
||||
},
|
||||
fetchBotSummary() {
|
||||
try {
|
||||
this.$store.dispatch('fetchBotSummary', this.getRequestPayload());
|
||||
} catch {
|
||||
this.showAlert(this.$t('REPORT.SUMMARY_FETCHING_FAILED'));
|
||||
}
|
||||
},
|
||||
fetchChartData() {
|
||||
Object.keys(this.reportKeys).forEach(async key => {
|
||||
try {
|
||||
await this.$store.dispatch('fetchAccountReport', {
|
||||
metric: this.reportKeys[key],
|
||||
...this.getRequestPayload(),
|
||||
});
|
||||
} catch {
|
||||
this.showAlert(this.$t('REPORT.DATA_FETCHING_FAILED'));
|
||||
}
|
||||
});
|
||||
},
|
||||
getRequestPayload() {
|
||||
const { from, to, groupBy, businessHours } = this;
|
||||
|
||||
return {
|
||||
from,
|
||||
to,
|
||||
groupBy: groupBy?.period,
|
||||
businessHours,
|
||||
};
|
||||
},
|
||||
onFilterChange({ from, to, groupBy, businessHours }) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.groupBy = groupBy;
|
||||
this.businessHours = businessHours;
|
||||
this.fetchAllData();
|
||||
|
||||
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
|
||||
filterValue: { from, to, groupBy, businessHours },
|
||||
reportType: 'bots',
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -7,7 +7,7 @@
|
||||
:key="metric.KEY"
|
||||
class="p-4 rounded-md mb-3"
|
||||
>
|
||||
<chart-stats :metric="metric" />
|
||||
<chart-stats :metric="metric" :account-summary-key="accountSummaryKey" />
|
||||
<div class="mt-4 h-72">
|
||||
<woot-loading-state
|
||||
v-if="accountReport.isFetching[metric.KEY]"
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import ReportMetricCard from './ReportMetricCard.vue';
|
||||
import ReportsAPI from 'dashboard/api/reports';
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const conversationCount = ref('0');
|
||||
const messageCount = ref('0');
|
||||
const resolutionRate = ref('0');
|
||||
const handoffRate = ref('0');
|
||||
|
||||
const formatToPercent = value => {
|
||||
return value ? `${value}%` : '--';
|
||||
};
|
||||
|
||||
const fetchMetrics = () => {
|
||||
if (!props.filters.to || !props.filters.from) {
|
||||
return;
|
||||
}
|
||||
ReportsAPI.getBotMetrics(props.filters).then(response => {
|
||||
conversationCount.value = response.data.conversation_count.toLocaleString();
|
||||
messageCount.value = response.data.message_count.toLocaleString();
|
||||
resolutionRate.value = response.data.resolution_rate.toString();
|
||||
handoffRate.value = response.data.handoff_rate.toString();
|
||||
});
|
||||
};
|
||||
|
||||
watch(() => props.filters, fetchMetrics, { deep: true });
|
||||
|
||||
onMounted(fetchMetrics);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-wrap mx-0 bg-white dark:bg-slate-800 rounded-[4px] p-4 mb-5 border border-solid border-slate-75 dark:border-slate-700"
|
||||
>
|
||||
<report-metric-card
|
||||
:label="$t('BOT_REPORTS.METRIC.TOTAL_CONVERSATIONS.LABEL')"
|
||||
:info-text="$t('BOT_REPORTS.METRIC.TOTAL_CONVERSATIONS.TOOLTIP')"
|
||||
:value="conversationCount"
|
||||
class="flex-1"
|
||||
/>
|
||||
<report-metric-card
|
||||
:label="$t('BOT_REPORTS.METRIC.TOTAL_RESPONSES.LABEL')"
|
||||
:info-text="$t('BOT_REPORTS.METRIC.TOTAL_RESPONSES.TOOLTIP')"
|
||||
:value="messageCount"
|
||||
class="flex-1"
|
||||
/>
|
||||
<report-metric-card
|
||||
:label="$t('BOT_REPORTS.METRIC.RESOLUTION_RATE.LABEL')"
|
||||
:info-text="$t('BOT_REPORTS.METRIC.RESOLUTION_RATE.TOOLTIP')"
|
||||
:value="formatToPercent(resolutionRate)"
|
||||
class="flex-1"
|
||||
/>
|
||||
<report-metric-card
|
||||
:label="$t('BOT_REPORTS.METRIC.HANDOFF_RATE.LABEL')"
|
||||
:info-text="$t('BOT_REPORTS.METRIC.HANDOFF_RATE.TOOLTIP')"
|
||||
:value="formatToPercent(handoffRate)"
|
||||
class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="text-sm">{{ metric.NAME }}</span>
|
||||
<div class="text-slate-900 dark:text-slate-100">
|
||||
<span class="text-sm">
|
||||
{{ metric.NAME }}
|
||||
</span>
|
||||
<div class="flex items-end">
|
||||
<div class="font-medium text-xl">
|
||||
{{ displayMetric(metric.KEY) }}
|
||||
|
||||
@@ -6,17 +6,20 @@
|
||||
:label="$t('CSAT_REPORTS.METRIC.TOTAL_RESPONSES.LABEL')"
|
||||
:info-text="$t('CSAT_REPORTS.METRIC.TOTAL_RESPONSES.TOOLTIP')"
|
||||
:value="responseCount"
|
||||
class="xs:w-full sm:max-w-[50%] lg:w-1/6 lg:max-w-[16%]"
|
||||
/>
|
||||
<csat-metric-card
|
||||
:disabled="ratingFilterEnabled"
|
||||
:label="$t('CSAT_REPORTS.METRIC.SATISFACTION_SCORE.LABEL')"
|
||||
:info-text="$t('CSAT_REPORTS.METRIC.SATISFACTION_SCORE.TOOLTIP')"
|
||||
:value="ratingFilterEnabled ? '--' : formatToPercent(satisfactionScore)"
|
||||
class="xs:w-full sm:max-w-[50%] lg:w-1/6 lg:max-w-[16%]"
|
||||
/>
|
||||
<csat-metric-card
|
||||
:label="$t('CSAT_REPORTS.METRIC.RESPONSE_RATE.LABEL')"
|
||||
:info-text="$t('CSAT_REPORTS.METRIC.RESPONSE_RATE.TOOLTIP')"
|
||||
:value="formatToPercent(responseRate)"
|
||||
class="xs:w-full sm:max-w-[50%] lg:w-1/6 lg:max-w-[16%]"
|
||||
/>
|
||||
|
||||
<div
|
||||
|
||||
@@ -21,7 +21,7 @@ defineProps({
|
||||
<template>
|
||||
<div
|
||||
ref="reportMetricContainer"
|
||||
class="xs:w-full sm:max-w-[50%] lg:w-1/6 lg:max-w-[16%] m-0 p-4"
|
||||
class="m-0 p-4"
|
||||
:class="{
|
||||
'grayscale pointer-events-none opacity-30': disabled,
|
||||
}"
|
||||
|
||||
@@ -194,6 +194,8 @@ export const METRIC_CHART = {
|
||||
reply_time: TIME_CHART_CONFIG,
|
||||
avg_resolution_time: TIME_CHART_CONFIG,
|
||||
resolutions_count: DEFAULT_CHART,
|
||||
bot_resolutions_count: DEFAULT_CHART,
|
||||
bot_handoffs_count: DEFAULT_CHART,
|
||||
};
|
||||
|
||||
export const OVERVIEW_METRICS = {
|
||||
|
||||
@@ -7,6 +7,7 @@ const LabelReports = () => import('./LabelReports.vue');
|
||||
const InboxReports = () => import('./InboxReports.vue');
|
||||
const TeamReports = () => import('./TeamReports.vue');
|
||||
const CsatResponses = () => import('./CsatResponses.vue');
|
||||
const BotReports = () => import('./BotReports.vue');
|
||||
const LiveReports = () => import('./LiveReports.vue');
|
||||
|
||||
export default {
|
||||
@@ -66,6 +67,23 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/reports'),
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'BOT_REPORTS.HEADER',
|
||||
icon: 'bot',
|
||||
keepAlive: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'bot',
|
||||
name: 'bot_reports',
|
||||
roles: ['administrator'],
|
||||
component: BotReports,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/reports'),
|
||||
component: SettingsContent,
|
||||
|
||||
Reference in New Issue
Block a user