@webitel/api-services 26.8.59 → 26.8.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webitel/api-services",
3
- "version": "26.8.59",
3
+ "version": "26.8.61",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,14 +1,16 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
- import { QueueType } from '../../../../enums';
3
+ import { QueuePeriod, QueueType } from '../../../../enums';
4
4
 
5
5
  const readQueue = vi.fn();
6
6
  const searchQueue = vi.fn();
7
+ const searchQueueReportGeneral = vi.fn();
7
8
 
8
9
  vi.mock('../../../../gen-wire', () => ({
9
10
  getQueueService: () => ({
10
11
  readQueue,
11
12
  searchQueue,
13
+ searchQueueReportGeneral,
12
14
  }),
13
15
  }));
14
16
 
@@ -128,3 +130,128 @@ describe('QueuesAPI.getList, on type-less rows', () => {
128
130
  });
129
131
  });
130
132
  });
133
+
134
+ /**
135
+ * `queuePeriod` is a relative-window preset the supervisor queues table sends
136
+ * ('today', '3hour', …); the service only understands an absolute
137
+ * `joined_at.from`/`joined_at.to` range, so resolving the preset to
138
+ * timestamps happens inside `getReportGeneral` itself.
139
+ */
140
+ describe('QueuesAPI.getReportGeneral, queuePeriod window resolution', () => {
141
+ beforeEach(() => {
142
+ searchQueueReportGeneral.mockReset();
143
+ searchQueueReportGeneral.mockResolvedValue({
144
+ data: {
145
+ items: [],
146
+ next: false,
147
+ },
148
+ });
149
+ });
150
+
151
+ it('defaults to today (start of day through now)', async () => {
152
+ const end = Date.now();
153
+
154
+ await QueuesAPI.getReportGeneral({});
155
+
156
+ const sentParams = searchQueueReportGeneral.mock.calls[0][0];
157
+ const expectedFrom = new Date(end).setHours(0, 0, 0, 0);
158
+
159
+ // checking approx equality: `Date.now()` here and inside the call may
160
+ // not be perfectly in sync
161
+ expect(sentParams['joined_at.from'].slice(0, -4)).toEqual(
162
+ `${expectedFrom}`.slice(0, -4),
163
+ );
164
+ expect(sentParams['joined_at.to'].slice(0, -4)).toEqual(
165
+ `${end}`.slice(0, -4),
166
+ );
167
+ });
168
+
169
+ it('resolves "3hour" to now minus 3 hours', async () => {
170
+ const end = Date.now();
171
+
172
+ await QueuesAPI.getReportGeneral({
173
+ queuePeriod: QueuePeriod.ThreeHours,
174
+ });
175
+
176
+ const sentParams = searchQueueReportGeneral.mock.calls[0][0];
177
+ const expectedFrom = end - 3 * 60 * 60 * 1000;
178
+
179
+ expect(sentParams['joined_at.from'].slice(0, -3)).toEqual(
180
+ `${expectedFrom}`.slice(0, -3),
181
+ );
182
+ expect(sentParams['joined_at.to'].slice(0, -3)).toEqual(
183
+ `${end}`.slice(0, -3),
184
+ );
185
+ });
186
+ });
187
+
188
+ /**
189
+ * The table renders bridged/abandoned/sl20/sl30 as percent strings and
190
+ * duration fields rounded to 2 decimals directly, with no caller-side
191
+ * formatting step; a queue with no matching agents omits `agent_status`
192
+ * entirely, so it has to be defaulted here too.
193
+ */
194
+ describe('QueuesAPI.getReportGeneral, response shaping', () => {
195
+ beforeEach(() => {
196
+ searchQueueReportGeneral.mockReset();
197
+ });
198
+
199
+ it('formats percentages, rounds durations, and defaults agentStatus/aggs', async () => {
200
+ searchQueueReportGeneral.mockResolvedValue({
201
+ data: {
202
+ items: [
203
+ {
204
+ processed: 1,
205
+ count: 1,
206
+ sum_bill_sec: 60,
207
+ avg_wrap_sec: 60,
208
+ avg_asa_sec: 60,
209
+ avg_awt_sec: 60,
210
+ },
211
+ ],
212
+ next: false,
213
+ },
214
+ });
215
+
216
+ const response = await QueuesAPI.getReportGeneral({});
217
+
218
+ expect(response).toEqual({
219
+ aggs: {
220
+ online: 0,
221
+ offline: 0,
222
+ free: 0,
223
+ pause: 0,
224
+ total: 0,
225
+ },
226
+ items: [
227
+ {
228
+ _isSelected: false,
229
+ abandoned: 0,
230
+ agentStatus: {
231
+ free: 0,
232
+ offline: 0,
233
+ online: 0,
234
+ pause: 0,
235
+ total: 0,
236
+ },
237
+ avgAhtSec: 0,
238
+ avgAsaSec: 60,
239
+ avgAwtSec: 60,
240
+ avgWrapSec: 60,
241
+ bridged: 0,
242
+ count: 1,
243
+ members: {
244
+ processing: 1,
245
+ waiting: 0,
246
+ },
247
+ processed: 1,
248
+ sumBillSec: 60,
249
+ transferred: 0,
250
+ sl20: 0,
251
+ sl30: 0,
252
+ },
253
+ ],
254
+ next: false,
255
+ });
256
+ });
257
+ });
@@ -3,7 +3,7 @@ import { queueSchema } from '@webitel/api-services/validations';
3
3
  import deepCopy from 'deep-copy';
4
4
  import deepmerge from 'deepmerge';
5
5
  import { isEmpty } from 'lodash-es';
6
- import { QueueType } from '../../../enums';
6
+ import { QueuePeriod, QueueType } from '../../../enums';
7
7
  import { getQueueService } from '../../../gen-wire';
8
8
  import { getDefaultGetListResponse, getDefaultGetParams } from '../../defaults';
9
9
  import {
@@ -287,16 +287,66 @@ export {
287
287
  QueueTypeDefaults,
288
288
  } from './defaults/queueTypeDefaults';
289
289
 
290
+ const resolveJoinedAtWindow = (
291
+ queuePeriod: QueuePeriod = QueuePeriod.Today,
292
+ ) => {
293
+ const end = new Date();
294
+ let start: Date;
295
+
296
+ const hour = 60 * 60 * 1000;
297
+ const min = 60 * 1000;
298
+
299
+ switch (queuePeriod) {
300
+ case QueuePeriod.SixHours:
301
+ start = new Date(end.getTime() - 6 * hour);
302
+ break;
303
+ case QueuePeriod.ThreeHours:
304
+ start = new Date(end.getTime() - 3 * hour);
305
+ break;
306
+ case QueuePeriod.OneHour:
307
+ start = new Date(end.getTime() - hour);
308
+ break;
309
+ case QueuePeriod.ThirtyMinutes:
310
+ start = new Date(end.getTime() - 30 * min);
311
+ break;
312
+ case QueuePeriod.FifteenMinutes:
313
+ start = new Date(end.getTime() - 15 * min);
314
+ break;
315
+ default:
316
+ start = new Date(end);
317
+ start.setHours(0, 0, 0, 0);
318
+ break;
319
+ }
320
+
321
+ return {
322
+ joinedAtFrom: start.getTime(),
323
+ joinedAtTo: end.getTime(),
324
+ };
325
+ };
326
+
327
+ const defaultAgentStatusObject = {
328
+ total: 0,
329
+ online: 0,
330
+ pause: 0,
331
+ offline: 0,
332
+ free: 0,
333
+ };
334
+
335
+ const asPercent = (value: number) => (value ? `${+value.toFixed(2)}%` : 0);
336
+ const rounded = (value: number) => (value ? +value.toFixed(2) : 0);
337
+
290
338
  /**
291
339
  * Aggregated queue performance over a joined-at window — the supervisor queues
292
- * table. Percentages and durations come back raw.
340
+ * table. Bridged/abandoned/sl20/sl30 arrive as percent strings and durations
341
+ * are rounded to 2 decimals so the table renders them without a caller-side
342
+ * formatting step; `agentStatus` and `aggs` are defaulted to zero values since
343
+ * a queue with no matching agents omits the field entirely.
293
344
  */
294
345
  const getQueuesReportGeneral = async (params: ApiParams) => {
295
346
  const {
296
347
  page,
297
348
  size,
298
- joinedAtFrom,
299
- joinedAtTo,
349
+ queuePeriod,
300
350
  fields,
301
351
  sort,
302
352
  search,
@@ -305,15 +355,20 @@ const getQueuesReportGeneral = async (params: ApiParams) => {
305
355
  queueType,
306
356
  } = applyTransform(params, [
307
357
  merge(getDefaultGetParams()),
358
+ merge({
359
+ search: '',
360
+ sort: '+priority',
361
+ }),
308
362
  starToSearch('search'),
309
363
  ]);
364
+ const { joinedAtFrom, joinedAtTo } = resolveJoinedAtWindow(queuePeriod);
310
365
 
311
366
  try {
312
367
  const response = await getQueueService().searchQueueReportGeneral({
313
368
  page,
314
369
  size,
315
- 'joined_at.from': joinedAtFrom,
316
- 'joined_at.to': joinedAtTo,
370
+ 'joined_at.from': String(joinedAtFrom),
371
+ 'joined_at.to': String(joinedAtTo),
317
372
  fields,
318
373
  sort,
319
374
  // the generated param is `q`; `search` is what the datalist store sends
@@ -327,8 +382,33 @@ const getQueuesReportGeneral = async (params: ApiParams) => {
327
382
  merge(getDefaultGetListResponse()),
328
383
  ]);
329
384
  return {
330
- items,
331
- aggs,
385
+ items: items.map((item: ApiParams) => ({
386
+ ...item,
387
+ _isSelected: false,
388
+ count: item.count || 0,
389
+ transferred: item.transferred || 0,
390
+ bridged: asPercent(item.bridged),
391
+ abandoned: asPercent(item.abandoned),
392
+ sumBillSec: rounded(item.sumBillSec),
393
+ avgWrapSec: rounded(item.avgWrapSec),
394
+ avgAsaSec: rounded(item.avgAsaSec),
395
+ avgAwtSec: rounded(item.avgAwtSec),
396
+ avgAhtSec: rounded(item.avgAhtSec),
397
+ sl20: asPercent(item.sl20),
398
+ sl30: asPercent(item.sl30),
399
+ agentStatus: {
400
+ ...defaultAgentStatusObject,
401
+ ...item.agentStatus,
402
+ },
403
+ members: {
404
+ processing: item.processed || 0,
405
+ waiting: item.waiting || 0,
406
+ },
407
+ })),
408
+ aggs: {
409
+ ...defaultAgentStatusObject,
410
+ ...aggs,
411
+ },
332
412
  next,
333
413
  };
334
414
  } catch (err) {
@@ -0,0 +1,10 @@
1
+ export const QueuePeriod = {
2
+ Today: 'today',
3
+ SixHours: '6hour',
4
+ ThreeHours: '3hour',
5
+ OneHour: '1hour',
6
+ ThirtyMinutes: '30min',
7
+ FifteenMinutes: '15min',
8
+ } as const;
9
+
10
+ export type QueuePeriod = (typeof QueuePeriod)[keyof typeof QueuePeriod];
@@ -2,6 +2,7 @@ import { CallReportingStatus } from './CallReportingStatus/CallReportingStatus';
2
2
  import { ChatGatewayProvider } from './ChatGatewayProvider/ChatGatewayPrivider';
3
3
  import { FormatDateMode } from './FormatDateMode/FormatDateMode';
4
4
  import { MemberStopCause } from './MemberStopCause/MemberStopCause';
5
+ import { QueuePeriod } from './QueuePeriod/QueuePeriod';
5
6
  import {
6
7
  QueueStrategy,
7
8
  QueueStrategyList,
@@ -18,6 +19,7 @@ export {
18
19
  ChatGatewayProvider,
19
20
  FormatDateMode,
20
21
  MemberStopCause,
22
+ QueuePeriod,
21
23
  QueueStrategy,
22
24
  QueueStrategyList,
23
25
  QueueType,