## Description Adds drilldown support for report bar charts powered by `ReportContainer`. Clicking a non-zero report bar now opens a right-side drawer with the conversations or messages that contributed to that bucket, with each row linking to the underlying conversation and message rows linking with `messageId`. This includes a new `GET /api/v2/accounts/:account_id/reports/drilldown` endpoint, backend drilldown builders/serializers, generic chart click emission, local drawer state via `useReportDrilldown`, compact drilldown cards, pagination, stale-response protection, and validation for unsupported drilldown dimensions. Fixes # CW-4497 https://linear.app/chatwoot/issue/CW-4497/drill-down-on-agent-conversations-report ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? Ran the focused backend and frontend checks for the drilldown endpoint, builder, chart click handling, drawer/card UI, API helper, and stale-response handling. Here are the screenshots on how it looks like: <img width="1792" height="1199" alt="Screenshot 2026-06-02 at 11 32 11 PM" src="https://github.com/user-attachments/assets/6bdb8832-b9df-4bf3-9a2a-beaefe203b6e" /> <img width="1791" height="1230" alt="Screenshot 2026-06-02 at 11 32 34 PM" src="https://github.com/user-attachments/assets/36e92eb7-3208-4855-87f4-0c7f316df54d" /> <img width="1784" height="1235" alt="Screenshot 2026-06-02 at 11 32 46 PM" src="https://github.com/user-attachments/assets/f7a53916-74f2-4622-9305-042e0ac9e877" /> ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
225 lines
6.2 KiB
JavaScript
225 lines
6.2 KiB
JavaScript
import reportsAPI from '../reports';
|
|
import ApiClient from '../ApiClient';
|
|
|
|
const timezoneOffset = () => -new Date().getTimezoneOffset() / 60;
|
|
|
|
describe('#Reports API', () => {
|
|
it('creates correct instance', () => {
|
|
expect(reportsAPI).toBeInstanceOf(ApiClient);
|
|
expect(reportsAPI.apiVersion).toBe('/api/v2');
|
|
expect(reportsAPI).toHaveProperty('get');
|
|
expect(reportsAPI).toHaveProperty('show');
|
|
expect(reportsAPI).toHaveProperty('create');
|
|
expect(reportsAPI).toHaveProperty('update');
|
|
expect(reportsAPI).toHaveProperty('delete');
|
|
expect(reportsAPI).toHaveProperty('getReports');
|
|
expect(reportsAPI).toHaveProperty('getDrilldown');
|
|
expect(reportsAPI).toHaveProperty('getSummary');
|
|
expect(reportsAPI).toHaveProperty('getAgentReports');
|
|
expect(reportsAPI).toHaveProperty('getLabelReports');
|
|
expect(reportsAPI).toHaveProperty('getInboxReports');
|
|
expect(reportsAPI).toHaveProperty('getTeamReports');
|
|
});
|
|
describe('API calls', () => {
|
|
const originalAxios = window.axios;
|
|
const axiosMock = {
|
|
post: vi.fn(() => Promise.resolve()),
|
|
get: vi.fn(() => Promise.resolve()),
|
|
patch: vi.fn(() => Promise.resolve()),
|
|
delete: vi.fn(() => Promise.resolve()),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
window.axios = axiosMock;
|
|
});
|
|
|
|
afterEach(() => {
|
|
window.axios = originalAxios;
|
|
});
|
|
|
|
it('#getAccountReports', () => {
|
|
reportsAPI.getReports({
|
|
metric: 'conversations_count',
|
|
from: 1621103400,
|
|
to: 1621621800,
|
|
});
|
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
|
|
params: {
|
|
business_hours: undefined,
|
|
group_by: undefined,
|
|
id: undefined,
|
|
metric: 'conversations_count',
|
|
since: 1621103400,
|
|
until: 1621621800,
|
|
type: 'account',
|
|
timezone_offset: timezoneOffset(),
|
|
},
|
|
});
|
|
});
|
|
|
|
it('#getAccountSummary', () => {
|
|
reportsAPI.getSummary(1621103400, 1621621800);
|
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/summary', {
|
|
params: {
|
|
business_hours: undefined,
|
|
group_by: undefined,
|
|
id: undefined,
|
|
since: 1621103400,
|
|
timezone_offset: timezoneOffset(),
|
|
type: 'account',
|
|
until: 1621621800,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('#getDrilldown', () => {
|
|
reportsAPI.getDrilldown({
|
|
metric: 'incoming_messages_count',
|
|
bucketTimestamp: 1621103400,
|
|
from: 1621103400,
|
|
to: 1621621800,
|
|
type: 'inbox',
|
|
id: 1,
|
|
groupBy: 'day',
|
|
businessHours: false,
|
|
page: 2,
|
|
perPage: 25,
|
|
});
|
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/drilldown', {
|
|
params: {
|
|
metric: 'incoming_messages_count',
|
|
bucket_timestamp: 1621103400,
|
|
since: 1621103400,
|
|
until: 1621621800,
|
|
type: 'inbox',
|
|
id: 1,
|
|
group_by: 'day',
|
|
business_hours: false,
|
|
timezone_offset: timezoneOffset(),
|
|
page: 2,
|
|
per_page: 25,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('#getDrilldown with abort signal', () => {
|
|
const controller = new AbortController();
|
|
|
|
reportsAPI.getDrilldown({
|
|
metric: 'incoming_messages_count',
|
|
bucketTimestamp: 1621103400,
|
|
signal: controller.signal,
|
|
});
|
|
|
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/drilldown', {
|
|
params: {
|
|
metric: 'incoming_messages_count',
|
|
bucket_timestamp: 1621103400,
|
|
since: undefined,
|
|
until: undefined,
|
|
type: 'account',
|
|
id: undefined,
|
|
group_by: undefined,
|
|
business_hours: undefined,
|
|
timezone_offset: timezoneOffset(),
|
|
page: undefined,
|
|
per_page: undefined,
|
|
},
|
|
signal: controller.signal,
|
|
});
|
|
});
|
|
|
|
it('#getAgentReports', () => {
|
|
reportsAPI.getAgentReports({
|
|
from: 1621103400,
|
|
to: 1621621800,
|
|
businessHours: true,
|
|
});
|
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/agents', {
|
|
params: {
|
|
since: 1621103400,
|
|
until: 1621621800,
|
|
business_hours: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('#getLabelReports', () => {
|
|
reportsAPI.getLabelReports({ from: 1621103400, to: 1621621800 });
|
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/labels', {
|
|
params: {
|
|
since: 1621103400,
|
|
until: 1621621800,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('#getInboxReports', () => {
|
|
reportsAPI.getInboxReports({ from: 1621103400, to: 1621621800 });
|
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/inboxes', {
|
|
params: {
|
|
since: 1621103400,
|
|
until: 1621621800,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('#getTeamReports', () => {
|
|
reportsAPI.getTeamReports({ from: 1621103400, to: 1621621800 });
|
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/teams', {
|
|
params: {
|
|
since: 1621103400,
|
|
until: 1621621800,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('#getBotMetrics', () => {
|
|
reportsAPI.getBotMetrics({ from: 1621103400, to: 1621621800 });
|
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
|
'/api/v2/reports/bot_metrics',
|
|
{
|
|
params: {
|
|
since: 1621103400,
|
|
until: 1621621800,
|
|
},
|
|
}
|
|
);
|
|
});
|
|
|
|
it('#getBotSummary', () => {
|
|
reportsAPI.getBotSummary({
|
|
from: 1621103400,
|
|
to: 1621621800,
|
|
groupBy: 'date',
|
|
businessHours: true,
|
|
});
|
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
|
'/api/v2/reports/bot_summary',
|
|
{
|
|
params: {
|
|
since: 1621103400,
|
|
until: 1621621800,
|
|
type: 'account',
|
|
group_by: 'date',
|
|
business_hours: true,
|
|
},
|
|
}
|
|
);
|
|
});
|
|
|
|
it('#getConversationMetric', () => {
|
|
reportsAPI.getConversationMetric('account');
|
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
|
'/api/v2/reports/conversations',
|
|
{
|
|
params: {
|
|
type: 'account',
|
|
page: 1,
|
|
},
|
|
}
|
|
);
|
|
});
|
|
});
|
|
});
|