Files
chatwoot/app/javascript/dashboard/store/modules/reports.js
Shivam Mishra f1f1ce644c 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.

<img width="1926" height="1670" alt="CleanShot 2025-10-13 at 14 01
35@2x"
src="https://github.com/user-attachments/assets/67822a34-6170-4d19-9e11-7ad4ded5c388"
/>

<img width="1964" height="1634" alt="CleanShot 2025-10-13 at 14 03
00@2x"
src="https://github.com/user-attachments/assets/e4613c08-64b8-4fa6-91d8-7510946dd75d"
/>


Unrelated change, the data seeder conversation resolution would not work
correctly, we've fixed it.

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2025-10-13 19:15:57 +05:30

355 lines
11 KiB
JavaScript

/* eslint no-console: 0 */
import * as types from '../mutation-types';
import { STATUS } from '../constants';
import Report from '../../api/reports';
import { downloadCsvFile, generateFileName } from '../../helper/downloadHelper';
import AnalyticsHelper from '../../helper/AnalyticsHelper';
import { REPORTS_EVENTS } from '../../helper/AnalyticsHelper/events';
import { clampDataBetweenTimeline } from 'shared/helpers/ReportsDataHelper';
import liveReports from '../../api/liveReports';
const state = {
fetchingStatus: false,
accountSummaryFetchingStatus: STATUS.FINISHED,
botSummaryFetchingStatus: STATUS.FINISHED,
accountReport: {
isFetching: {
conversations_count: false,
incoming_messages_count: false,
outgoing_messages_count: false,
avg_first_response_time: false,
avg_resolution_time: false,
resolutions_count: false,
bot_resolutions_count: false,
bot_handoffs_count: false,
reply_time: false,
},
data: {
conversations_count: [],
incoming_messages_count: [],
outgoing_messages_count: [],
avg_first_response_time: [],
avg_resolution_time: [],
resolutions_count: [],
bot_resolutions_count: [],
bot_handoffs_count: [],
reply_time: [],
},
},
accountSummary: {
avg_first_response_time: 0,
avg_resolution_time: 0,
conversations_count: 0,
incoming_messages_count: 0,
outgoing_messages_count: 0,
reply_time: 0,
resolutions_count: 0,
bot_resolutions_count: 0,
bot_handoffs_count: 0,
previous: {},
},
botSummary: {
bot_resolutions_count: 0,
bot_handoffs_count: 0,
previous: {},
},
overview: {
uiFlags: {
isFetchingAccountConversationMetric: false,
isFetchingAccountConversationsHeatmap: false,
isFetchingAccountResolutionsHeatmap: false,
isFetchingAgentConversationMetric: false,
isFetchingTeamConversationMetric: false,
},
accountConversationMetric: {},
accountConversationHeatmap: [],
accountResolutionHeatmap: [],
agentConversationMetric: [],
teamConversationMetric: [],
},
};
const getters = {
getAccountReports(_state) {
return _state.accountReport;
},
getAccountSummary(_state) {
return _state.accountSummary;
},
getBotSummary(_state) {
return _state.botSummary;
},
getAccountSummaryFetchingStatus(_state) {
return _state.accountSummaryFetchingStatus;
},
getBotSummaryFetchingStatus(_state) {
return _state.botSummaryFetchingStatus;
},
getAccountConversationMetric(_state) {
return _state.overview.accountConversationMetric;
},
getAccountConversationHeatmapData(_state) {
return _state.overview.accountConversationHeatmap;
},
getAccountResolutionHeatmapData(_state) {
return _state.overview.accountResolutionHeatmap;
},
getAgentConversationMetric(_state) {
return _state.overview.agentConversationMetric;
},
getTeamConversationMetric(_state) {
return _state.overview.teamConversationMetric;
},
getOverviewUIFlags($state) {
return $state.overview.uiFlags;
},
};
export const actions = {
fetchAccountReport({ commit }, reportObj) {
const { metric } = reportObj;
commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, {
metric,
value: true,
});
Report.getReports(reportObj).then(accountReport => {
let { data } = accountReport;
data = clampDataBetweenTimeline(data, reportObj.from, reportObj.to);
commit(types.default.SET_ACCOUNT_REPORTS, {
metric,
data,
});
commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, {
metric,
value: false,
});
});
},
fetchAccountConversationHeatmap({ commit }, reportObj) {
commit(types.default.TOGGLE_HEATMAP_LOADING, true);
Report.getReports({ ...reportObj, groupBy: 'hour' }).then(heatmapData => {
let { data } = heatmapData;
data = clampDataBetweenTimeline(data, reportObj.from, reportObj.to);
commit(types.default.SET_HEATMAP_DATA, data);
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(
reportObj.from,
reportObj.to,
reportObj.type,
reportObj.id,
reportObj.groupBy,
reportObj.businessHours
)
.then(accountSummary => {
commit(types.default.SET_ACCOUNT_SUMMARY, accountSummary.data);
commit(types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FINISHED);
})
.catch(() => {
commit(types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FAILED);
});
},
fetchBotSummary({ commit }, reportObj) {
commit(types.default.SET_BOT_SUMMARY_STATUS, STATUS.FETCHING);
Report.getBotSummary({
from: reportObj.from,
to: reportObj.to,
groupBy: reportObj.groupBy,
businessHours: reportObj.businessHours,
})
.then(botSummary => {
commit(types.default.SET_BOT_SUMMARY, botSummary.data);
commit(types.default.SET_BOT_SUMMARY_STATUS, STATUS.FINISHED);
})
.catch(() => {
commit(types.default.SET_BOT_SUMMARY_STATUS, STATUS.FAILED);
});
},
fetchAccountConversationMetric({ commit }, params = {}) {
commit(types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING, true);
liveReports
.getConversationMetric(params)
.then(accountConversationMetric => {
commit(
types.default.SET_ACCOUNT_CONVERSATION_METRIC,
accountConversationMetric.data
);
commit(types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING, false);
})
.catch(() => {
commit(types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING, false);
});
},
fetchAgentConversationMetric({ commit }) {
commit(types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING, true);
liveReports
.getGroupedConversations({ groupBy: 'assignee_id' })
.then(agentConversationMetric => {
commit(
types.default.SET_AGENT_CONVERSATION_METRIC,
agentConversationMetric.data
);
commit(types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING, false);
})
.catch(() => {
commit(types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING, false);
});
},
fetchTeamConversationMetric({ commit }) {
commit(types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING, true);
liveReports
.getGroupedConversations({ groupBy: 'team_id' })
.then(teamMetric => {
commit(types.default.SET_TEAM_CONVERSATION_METRIC, teamMetric.data);
commit(types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING, false);
})
.catch(() => {
commit(types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING, false);
});
},
downloadAgentReports(_, reportObj) {
return Report.getAgentReports(reportObj)
.then(response => {
downloadCsvFile(reportObj.fileName, response.data);
AnalyticsHelper.track(REPORTS_EVENTS.DOWNLOAD_REPORT, {
reportType: 'agent',
businessHours: reportObj?.businessHours,
});
})
.catch(error => {
console.error(error);
});
},
downloadLabelReports(_, reportObj) {
return Report.getLabelReports(reportObj)
.then(response => {
downloadCsvFile(reportObj.fileName, response.data);
AnalyticsHelper.track(REPORTS_EVENTS.DOWNLOAD_REPORT, {
reportType: 'label',
businessHours: reportObj?.businessHours,
});
})
.catch(error => {
console.error(error);
});
},
downloadInboxReports(_, reportObj) {
return Report.getInboxReports(reportObj)
.then(response => {
downloadCsvFile(reportObj.fileName, response.data);
AnalyticsHelper.track(REPORTS_EVENTS.DOWNLOAD_REPORT, {
reportType: 'inbox',
businessHours: reportObj?.businessHours,
});
})
.catch(error => {
console.error(error);
});
},
downloadTeamReports(_, reportObj) {
return Report.getTeamReports(reportObj)
.then(response => {
downloadCsvFile(reportObj.fileName, response.data);
AnalyticsHelper.track(REPORTS_EVENTS.DOWNLOAD_REPORT, {
reportType: 'team',
businessHours: reportObj?.businessHours,
});
})
.catch(error => {
console.error(error);
});
},
downloadAccountConversationHeatmap(_, reportObj) {
Report.getConversationTrafficCSV({ daysBefore: reportObj.daysBefore })
.then(response => {
downloadCsvFile(
generateFileName({
type: 'Conversation traffic',
to: reportObj.to,
}),
response.data
);
AnalyticsHelper.track(REPORTS_EVENTS.DOWNLOAD_REPORT, {
reportType: 'conversation_heatmap',
businessHours: false,
});
})
.catch(error => {
console.error(error);
});
},
};
const mutations = {
[types.default.SET_ACCOUNT_REPORTS](_state, { metric, data }) {
_state.accountReport.data[metric] = data;
},
[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;
},
[types.default.SET_BOT_SUMMARY_STATUS](_state, status) {
_state.botSummaryFetchingStatus = status;
},
[types.default.SET_ACCOUNT_SUMMARY_STATUS](_state, status) {
_state.accountSummaryFetchingStatus = status;
},
[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;
},
[types.default.SET_BOT_SUMMARY](_state, summaryData) {
_state.botSummary = summaryData;
},
[types.default.SET_ACCOUNT_CONVERSATION_METRIC](_state, metricData) {
_state.overview.accountConversationMetric = metricData;
},
[types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING](_state, flag) {
_state.overview.uiFlags.isFetchingAccountConversationMetric = flag;
},
[types.default.SET_AGENT_CONVERSATION_METRIC](_state, metricData) {
_state.overview.agentConversationMetric = metricData;
},
[types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING](_state, flag) {
_state.overview.uiFlags.isFetchingAgentConversationMetric = flag;
},
[types.default.SET_TEAM_CONVERSATION_METRIC](_state, metricData) {
_state.overview.teamConversationMetric = metricData;
},
[types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING](_state, flag) {
_state.overview.uiFlags.isFetchingTeamConversationMetric = flag;
},
};
export default {
state,
getters,
actions,
mutations,
};