feat: introduce voice call dashboard (#14954)

This commit is contained in:
Sivin Varghese
2026-07-20 14:54:05 +05:30
committed by GitHub
parent a752e56765
commit bf0a10c780
20 changed files with 1426 additions and 5 deletions
@@ -0,0 +1,40 @@
import camelcaseKeys from 'camelcase-keys';
import CallsAPI from 'dashboard/api/calls';
import { throwErrorMessage } from 'dashboard/store/utils/api';
import { defineStore } from 'pinia';
export const useCallHistoryStore = defineStore('callHistory', {
state: () => ({
records: [],
meta: { count: 0, currentPage: 1, totalPages: 0 },
uiFlags: { isFetching: false },
fetchRequestToken: 0,
}),
actions: {
async fetchCalls(params = {}) {
this.uiFlags.isFetching = true;
this.fetchRequestToken += 1;
const requestToken = this.fetchRequestToken;
try {
const { data } = await CallsAPI.get(params);
// A newer fetch (filter/page change) superseded this one; drop the result.
if (this.fetchRequestToken !== requestToken) return this.records;
this.records = camelcaseKeys(data.payload, { deep: true });
this.meta = camelcaseKeys(data.meta);
return this.records;
} catch (error) {
// Don't surface errors from a fetch that a newer request already replaced.
if (this.fetchRequestToken !== requestToken) return this.records;
// Drop the previous results so stale rows aren't shown under the new view.
this.records = [];
this.meta = { count: 0, currentPage: 1, totalPages: 0 };
return throwErrorMessage(error);
} finally {
if (this.fetchRequestToken === requestToken) {
this.uiFlags.isFetching = false;
}
}
},
},
});
@@ -0,0 +1,117 @@
import { setActivePinia, createPinia } from 'pinia';
import CallsAPI from 'dashboard/api/calls';
import { throwErrorMessage } from 'dashboard/store/utils/api';
import { useCallHistoryStore } from '../callHistory';
vi.mock('dashboard/api/calls', () => ({
default: {
get: vi.fn(),
},
}));
vi.mock('dashboard/store/utils/api', () => ({
throwErrorMessage: vi.fn(error => error),
}));
const createDeferred = () => {
let resolve;
const promise = new Promise(res => {
resolve = res;
});
return { promise, resolve };
};
const buildResponse = (payload, meta) => ({ data: { payload, meta } });
describe('callHistory store', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
});
it('fetches calls and stores camelized records and meta', async () => {
CallsAPI.get.mockResolvedValue(
buildResponse(
[{ id: 1, recording_url: 'rec.mp3', contact: { phone_number: '+1' } }],
{ count: 44, current_page: 1, total_pages: 2 }
)
);
const store = useCallHistoryStore();
await store.fetchCalls({ page: 1, status: 'no-answer' });
expect(CallsAPI.get).toHaveBeenCalledWith({ page: 1, status: 'no-answer' });
expect(store.records).toEqual([
{ id: 1, recordingUrl: 'rec.mp3', contact: { phoneNumber: '+1' } },
]);
expect(store.meta).toEqual({ count: 44, currentPage: 1, totalPages: 2 });
expect(store.uiFlags.isFetching).toBe(false);
});
it('drops a superseded response that resolves after the latest one', async () => {
const firstRequest = createDeferred();
const secondRequest = createDeferred();
CallsAPI.get
.mockImplementationOnce(() => firstRequest.promise)
.mockImplementationOnce(() => secondRequest.promise);
const store = useCallHistoryStore();
const staleFetch = store.fetchCalls({ page: 1 });
const currentFetch = store.fetchCalls({ page: 2 });
secondRequest.resolve(
buildResponse([{ id: 2 }], { count: 1, current_page: 2, total_pages: 2 })
);
await currentFetch;
firstRequest.resolve(
buildResponse([{ id: 1 }], { count: 99, current_page: 1, total_pages: 9 })
);
await staleFetch;
expect(store.records).toEqual([{ id: 2 }]);
expect(store.meta.count).toBe(1);
expect(store.uiFlags.isFetching).toBe(false);
});
it('keeps fetching state when a superseded response resolves first', async () => {
const firstRequest = createDeferred();
const secondRequest = createDeferred();
CallsAPI.get
.mockImplementationOnce(() => firstRequest.promise)
.mockImplementationOnce(() => secondRequest.promise);
const store = useCallHistoryStore();
const staleFetch = store.fetchCalls({ page: 1 });
const currentFetch = store.fetchCalls({ page: 2 });
firstRequest.resolve(
buildResponse([{ id: 1 }], { count: 99, current_page: 1, total_pages: 9 })
);
await staleFetch;
expect(store.records).toEqual([]);
expect(store.uiFlags.isFetching).toBe(true);
secondRequest.resolve(
buildResponse([{ id: 2 }], { count: 1, current_page: 2, total_pages: 2 })
);
await currentFetch;
expect(store.records).toEqual([{ id: 2 }]);
expect(store.uiFlags.isFetching).toBe(false);
});
it('surfaces the error and resets fetching state on failure', async () => {
const error = new Error('Request failed');
CallsAPI.get.mockRejectedValue(error);
const store = useCallHistoryStore();
await store.fetchCalls();
expect(throwErrorMessage).toHaveBeenCalledWith(error);
expect(store.records).toEqual([]);
expect(store.uiFlags.isFetching).toBe(false);
});
});
@@ -1,6 +1,6 @@
import { setActivePinia, createPinia } from 'pinia';
import CompanyAPI from 'dashboard/api/companies';
import { useCompaniesStore } from './companies';
import { useCompaniesStore } from '../companies';
vi.mock('dashboard/api/companies', () => ({
default: {