@webitel/api-services 26.8.56 → 26.8.58
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 +1 -1
- package/src/api/clients/agents/__tests__/agentStatusStatistics.spec.ts +45 -0
- package/src/api/clients/agents/agents.ts +56 -11
- package/src/api/clients/fileServices/fileServices.ts +21 -1
- package/src/api/clients/pdfServices/pdfServices.ts +21 -1
- package/src/enums/UrlProtocol/UrlProtocol.ts +7 -0
- package/src/enums/index.ts +2 -0
- package/src/validations/case/case.validations.ts +35 -1
- package/src/validations/caseLink/__tests__/caseLink.validations.spec.ts +52 -0
- package/src/validations/caseLink/caseLink.validations.ts +24 -0
- package/src/validations/index.ts +1 -0
- package/types/.tsbuildinfo +1 -1
- package/types/api/clients/fileServices/fileServices.d.ts +3 -0
- package/types/api/clients/pdfServices/pdfServices.d.ts +3 -0
- package/types/enums/UrlProtocol/UrlProtocol.d.ts +6 -0
- package/types/enums/index.d.ts +2 -1
- package/types/validations/case/case.validations.d.ts +3 -2
- package/types/validations/caseLink/caseLink.validations.d.ts +8 -0
- package/types/validations/index.d.ts +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const searchAgentStatusStatistic = vi.fn(() =>
|
|
4
|
+
Promise.resolve({
|
|
5
|
+
data: {
|
|
6
|
+
items: [
|
|
7
|
+
{},
|
|
8
|
+
],
|
|
9
|
+
},
|
|
10
|
+
}),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
vi.mock('../../../../gen-wire', async (importOriginal) => ({
|
|
14
|
+
...(await importOriginal<Record<string, unknown>>()),
|
|
15
|
+
getAgentService: () => ({
|
|
16
|
+
searchAgentStatusStatistic,
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const { AgentsAPI } = await import('../agents');
|
|
21
|
+
|
|
22
|
+
const expectItems = [
|
|
23
|
+
{
|
|
24
|
+
_isSelected: false,
|
|
25
|
+
missed: 0,
|
|
26
|
+
transferred: 0,
|
|
27
|
+
statusDuration: '00:00:00',
|
|
28
|
+
utilization: '0.00%',
|
|
29
|
+
online: '00:00:00',
|
|
30
|
+
offline: '00:00:00',
|
|
31
|
+
pause: '00:00:00',
|
|
32
|
+
callTime: '00:00:00',
|
|
33
|
+
chatTime: '00:00:00',
|
|
34
|
+
occupancy: '0.00%',
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
describe('AgentsAPI.getStatusStatistics', () => {
|
|
39
|
+
it('formats the raw response', async () => {
|
|
40
|
+
const response = await AgentsAPI.getStatusStatistics({});
|
|
41
|
+
|
|
42
|
+
expect(response.items).toEqual(expectItems);
|
|
43
|
+
expect(response.next).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -215,11 +215,51 @@ const deleteAgent = async ({ id }: DeleteItemParams) => {
|
|
|
215
215
|
}
|
|
216
216
|
};
|
|
217
217
|
|
|
218
|
-
/**
|
|
219
|
-
* Per-agent status/occupancy statistics over a time window — the supervisor
|
|
220
|
-
* agents table. Durations come back raw; formatting them is the caller's job.
|
|
221
|
-
*/
|
|
218
|
+
/** Per-agent status/occupancy statistics over a time window — the supervisor agents table. */
|
|
222
219
|
const getAgentStatusStatistics = async (params: ApiParams) => {
|
|
220
|
+
const defaultParams = {
|
|
221
|
+
search: '',
|
|
222
|
+
sort: '+name',
|
|
223
|
+
fields: [],
|
|
224
|
+
from: new Date().setHours(0, 0, 0, 0),
|
|
225
|
+
to: new Date().setHours(23, 59, 59, 999),
|
|
226
|
+
utilizationFrom: '0',
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const defaultObject = {
|
|
230
|
+
offline: 0,
|
|
231
|
+
online: 0,
|
|
232
|
+
pause: 0,
|
|
233
|
+
statusDuration: 0,
|
|
234
|
+
transferred: 0,
|
|
235
|
+
missed: 0,
|
|
236
|
+
utilization: 0,
|
|
237
|
+
occupancy: 0,
|
|
238
|
+
callTime: 0,
|
|
239
|
+
chatTime: 0,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const listResponseHandler = (items: ApiParams[]) => {
|
|
243
|
+
return items.map((item) => {
|
|
244
|
+
const merged = {
|
|
245
|
+
...defaultObject,
|
|
246
|
+
...item,
|
|
247
|
+
};
|
|
248
|
+
return {
|
|
249
|
+
...merged,
|
|
250
|
+
_isSelected: false,
|
|
251
|
+
statusDuration: convertStatusDuration(merged.statusDuration),
|
|
252
|
+
utilization: `${merged.utilization.toFixed(2)}%`,
|
|
253
|
+
occupancy: `${merged.occupancy.toFixed(2)}%`,
|
|
254
|
+
online: convertDuration(merged.online),
|
|
255
|
+
offline: convertDuration(merged.offline),
|
|
256
|
+
pause: convertDuration(merged.pause),
|
|
257
|
+
callTime: convertDuration(merged.callTime),
|
|
258
|
+
chatTime: convertDuration(merged.chatTime),
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
|
|
223
263
|
const {
|
|
224
264
|
page,
|
|
225
265
|
size,
|
|
@@ -229,7 +269,6 @@ const getAgentStatusStatistics = async (params: ApiParams) => {
|
|
|
229
269
|
fields,
|
|
230
270
|
from,
|
|
231
271
|
to,
|
|
232
|
-
status,
|
|
233
272
|
queue,
|
|
234
273
|
team,
|
|
235
274
|
skill,
|
|
@@ -237,9 +276,13 @@ const getAgentStatusStatistics = async (params: ApiParams) => {
|
|
|
237
276
|
auditor,
|
|
238
277
|
region,
|
|
239
278
|
utilizationFrom,
|
|
240
|
-
|
|
279
|
+
// the filter is named after the progress bar it drives
|
|
280
|
+
utilizationProgress,
|
|
281
|
+
// the generated param is `status`; `agentStatus` is what the datalist store sends
|
|
282
|
+
agentStatus,
|
|
241
283
|
callNow,
|
|
242
284
|
} = applyTransform(params, [
|
|
285
|
+
merge(defaultParams),
|
|
243
286
|
merge(getDefaultGetParams()),
|
|
244
287
|
]);
|
|
245
288
|
|
|
@@ -252,13 +295,13 @@ const getAgentStatusStatistics = async (params: ApiParams) => {
|
|
|
252
295
|
sort,
|
|
253
296
|
fields,
|
|
254
297
|
agent_id: ids,
|
|
255
|
-
'time.from': from,
|
|
256
|
-
'time.to': to,
|
|
257
|
-
status,
|
|
298
|
+
'time.from': from == null ? undefined : String(from),
|
|
299
|
+
'time.to': to == null ? undefined : String(to),
|
|
300
|
+
status: agentStatus,
|
|
258
301
|
queue_id: queue,
|
|
259
302
|
team_id: team,
|
|
260
303
|
'utilization.from': utilizationFrom,
|
|
261
|
-
'utilization.to':
|
|
304
|
+
'utilization.to': utilizationProgress,
|
|
262
305
|
has_call: callNow,
|
|
263
306
|
skill_id: skill,
|
|
264
307
|
region_id: region,
|
|
@@ -270,7 +313,9 @@ const getAgentStatusStatistics = async (params: ApiParams) => {
|
|
|
270
313
|
merge(getDefaultGetListResponse()),
|
|
271
314
|
]);
|
|
272
315
|
return {
|
|
273
|
-
items,
|
|
316
|
+
items: applyTransform(items, [
|
|
317
|
+
listResponseHandler,
|
|
318
|
+
]),
|
|
274
319
|
next,
|
|
275
320
|
};
|
|
276
321
|
} catch (err) {
|
|
@@ -18,6 +18,11 @@ import type {
|
|
|
18
18
|
SearchScreenRecordingsByAgentParams as SearchScreenRecordingsByAgentWireParams,
|
|
19
19
|
SearchScreenRecordingsParams as SearchScreenRecordingsWireParams,
|
|
20
20
|
} from '../../../gen-wire/_models';
|
|
21
|
+
import type {
|
|
22
|
+
DatetimeRangeValue,
|
|
23
|
+
NormalizeDatetimeValueParam,
|
|
24
|
+
} from '../../../scripts';
|
|
25
|
+
import { normalizeDatetimeRange } from '../../../scripts';
|
|
21
26
|
import { getDefaultGetListResponse, getDefaultGetParams } from '../../defaults';
|
|
22
27
|
import {
|
|
23
28
|
applyTransform,
|
|
@@ -159,14 +164,29 @@ const getScreenRecordingsByAgent = async (
|
|
|
159
164
|
params: SearchScreenRecordingsByAgentParams & {
|
|
160
165
|
agentId: ApiId;
|
|
161
166
|
search?: string;
|
|
167
|
+
/** the datalist store's uploadedAt date-range filter sends this combined value */
|
|
168
|
+
uploadedAt?: DatetimeRangeValue | NormalizeDatetimeValueParam;
|
|
162
169
|
},
|
|
163
170
|
) => {
|
|
171
|
+
const { uploadedAt, ...restParams } = params;
|
|
172
|
+
const normalizedUploadedAt = normalizeDatetimeRange(uploadedAt);
|
|
173
|
+
|
|
164
174
|
const fieldsToSend = getShallowFieldsToSendFromZodSchema(
|
|
165
175
|
SearchScreenRecordingsByAgentQueryParams,
|
|
166
176
|
);
|
|
167
177
|
|
|
168
178
|
const requestParams = applyTransform<SearchScreenRecordingsByAgentWireParams>(
|
|
169
|
-
|
|
179
|
+
{
|
|
180
|
+
...restParams,
|
|
181
|
+
uploadedAtFrom:
|
|
182
|
+
normalizedUploadedAt?.from == null
|
|
183
|
+
? restParams.uploadedAtFrom
|
|
184
|
+
: String(normalizedUploadedAt.from),
|
|
185
|
+
uploadedAtTo:
|
|
186
|
+
normalizedUploadedAt?.to == null
|
|
187
|
+
? restParams.uploadedAtTo
|
|
188
|
+
: String(normalizedUploadedAt.to),
|
|
189
|
+
},
|
|
170
190
|
[
|
|
171
191
|
merge(getDefaultGetParams()),
|
|
172
192
|
sanitizeToWire(fieldsToSend),
|
|
@@ -17,6 +17,11 @@ import type {
|
|
|
17
17
|
ListCallExportsParams as ListCallExportsWireParams,
|
|
18
18
|
ListScreenrecordingExportsParams as ListScreenrecordingExportsWireParams,
|
|
19
19
|
} from '../../../gen-wire/_models';
|
|
20
|
+
import type {
|
|
21
|
+
DatetimeRangeValue,
|
|
22
|
+
NormalizeDatetimeValueParam,
|
|
23
|
+
} from '../../../scripts';
|
|
24
|
+
import { normalizeDatetimeRange } from '../../../scripts';
|
|
20
25
|
import { getDefaultGetListResponse, getDefaultGetParams } from '../../defaults';
|
|
21
26
|
import {
|
|
22
27
|
applyTransform,
|
|
@@ -60,14 +65,29 @@ const createScreenrecordingExport = async ({
|
|
|
60
65
|
const listScreenrecordingExports = async (
|
|
61
66
|
params: ListScreenrecordingExportsParams & {
|
|
62
67
|
agentId: ApiId;
|
|
68
|
+
/** the datalist store's uploadedAt date-range filter sends this combined value */
|
|
69
|
+
uploadedAt?: DatetimeRangeValue | NormalizeDatetimeValueParam;
|
|
63
70
|
},
|
|
64
71
|
) => {
|
|
72
|
+
const { uploadedAt, ...restParams } = params;
|
|
73
|
+
const normalizedUploadedAt = normalizeDatetimeRange(uploadedAt);
|
|
74
|
+
|
|
65
75
|
const fieldsToSend = getShallowFieldsToSendFromZodSchema(
|
|
66
76
|
ListScreenrecordingExportsQueryParams,
|
|
67
77
|
);
|
|
68
78
|
|
|
69
79
|
const requestParams = applyTransform<ListScreenrecordingExportsWireParams>(
|
|
70
|
-
|
|
80
|
+
{
|
|
81
|
+
...restParams,
|
|
82
|
+
uploadedAtFrom:
|
|
83
|
+
normalizedUploadedAt?.from == null
|
|
84
|
+
? restParams.uploadedAtFrom
|
|
85
|
+
: String(normalizedUploadedAt.from),
|
|
86
|
+
uploadedAtTo:
|
|
87
|
+
normalizedUploadedAt?.to == null
|
|
88
|
+
? restParams.uploadedAtTo
|
|
89
|
+
: String(normalizedUploadedAt.to),
|
|
90
|
+
},
|
|
71
91
|
[
|
|
72
92
|
merge(getDefaultGetParams()),
|
|
73
93
|
sanitizeToWire(fieldsToSend),
|
package/src/enums/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { QueueType, QueueTypeName } from './QueueType/QueueType';
|
|
|
10
10
|
import { RelativeDatetimeValue } from './RelativeDatetimeValue/RelativeDatetimeValue';
|
|
11
11
|
import { TimeBaseScore } from './TimeBaseScore/TimeBaseScore';
|
|
12
12
|
import { TypesResourceStrategy } from './TypesResourceStrategy/TypesResourceStrategy';
|
|
13
|
+
import { UrlProtocol } from './UrlProtocol/UrlProtocol';
|
|
13
14
|
import { UserPresenceStatus } from './UserPresenceStatus/UserPresenceStatus';
|
|
14
15
|
|
|
15
16
|
export {
|
|
@@ -24,5 +25,6 @@ export {
|
|
|
24
25
|
RelativeDatetimeValue,
|
|
25
26
|
TimeBaseScore,
|
|
26
27
|
TypesResourceStrategy,
|
|
28
|
+
UrlProtocol,
|
|
27
29
|
UserPresenceStatus,
|
|
28
30
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { WebitelCasesCase } from '@webitel/api-services/gen/models';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
|
+
import { i18nIssue } from '../_shared/i18nIssue';
|
|
4
5
|
import { lookupSchema } from '../_shared/lookup.validations';
|
|
5
6
|
|
|
6
7
|
const lookupShape = () => lookupSchema.passthrough().default({});
|
|
@@ -28,6 +29,39 @@ export const caseSchema = z.object({
|
|
|
28
29
|
reporter: lookupShape(),
|
|
29
30
|
service: lookupShape(),
|
|
30
31
|
statusCondition: statusConditionShape(),
|
|
31
|
-
closeReason:
|
|
32
|
+
closeReason: lookupSchema.passthrough().nullish(),
|
|
32
33
|
closeResult: z.string().default(''),
|
|
33
34
|
} satisfies Partial<Record<keyof WebitelCasesCase, z.ZodType>>);
|
|
35
|
+
|
|
36
|
+
export const refineCaseCloseFields = (
|
|
37
|
+
data: Pick<
|
|
38
|
+
z.infer<typeof caseSchema>,
|
|
39
|
+
'statusCondition' | 'closeReason' | 'closeResult'
|
|
40
|
+
>,
|
|
41
|
+
ctx: z.RefinementCtx,
|
|
42
|
+
) => {
|
|
43
|
+
if (!data.statusCondition?.final) return;
|
|
44
|
+
|
|
45
|
+
if (!data.closeReason?.id)
|
|
46
|
+
ctx.addIssue({
|
|
47
|
+
code: 'custom',
|
|
48
|
+
path: data.closeReason
|
|
49
|
+
? [
|
|
50
|
+
'closeReason',
|
|
51
|
+
'id',
|
|
52
|
+
]
|
|
53
|
+
: [
|
|
54
|
+
'closeReason',
|
|
55
|
+
],
|
|
56
|
+
...i18nIssue('required'),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!data.closeResult)
|
|
60
|
+
ctx.addIssue({
|
|
61
|
+
code: 'custom',
|
|
62
|
+
path: [
|
|
63
|
+
'closeResult',
|
|
64
|
+
],
|
|
65
|
+
...i18nIssue('required'),
|
|
66
|
+
});
|
|
67
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { caseLinkSchema } from '../caseLink.validations';
|
|
4
|
+
|
|
5
|
+
const getIssues = (url: string) =>
|
|
6
|
+
caseLinkSchema.safeParse({
|
|
7
|
+
url,
|
|
8
|
+
}).error?.issues ?? [];
|
|
9
|
+
|
|
10
|
+
describe('caseLinkSchema', () => {
|
|
11
|
+
it.each([
|
|
12
|
+
'https://webitel.com',
|
|
13
|
+
'http://webitel.com/path?query=1',
|
|
14
|
+
'ftp://files.webitel.com',
|
|
15
|
+
])('accepts %s', (url) => {
|
|
16
|
+
expect(getIssues(url)).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('requires a url', () => {
|
|
20
|
+
const issues = getIssues('');
|
|
21
|
+
|
|
22
|
+
expect(issues).toHaveLength(1);
|
|
23
|
+
expect(issues[0].code).toBe('too_small');
|
|
24
|
+
expect(issues[0].path).toEqual([
|
|
25
|
+
'url',
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it.each([
|
|
30
|
+
'webitel',
|
|
31
|
+
'webitel.com',
|
|
32
|
+
'mailto:support@webitel.com',
|
|
33
|
+
'javascript:alert(1)',
|
|
34
|
+
])('rejects %s as not a url', (url) => {
|
|
35
|
+
const issues = getIssues(url);
|
|
36
|
+
|
|
37
|
+
expect(issues).toHaveLength(1);
|
|
38
|
+
expect(issues[0].code).toBe('custom');
|
|
39
|
+
expect(issues[0].params).toEqual({
|
|
40
|
+
i18nKey: 'url',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('keeps the link text optional', () => {
|
|
45
|
+
expect(
|
|
46
|
+
caseLinkSchema.safeParse({
|
|
47
|
+
url: 'https://webitel.com',
|
|
48
|
+
name: 'Webitel',
|
|
49
|
+
}).success,
|
|
50
|
+
).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { WebitelCasesInputCaseLink } from '@webitel/api-services/gen/models';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { UrlProtocol } from '../../enums';
|
|
5
|
+
import { i18nIssue } from '../_shared/i18nIssue';
|
|
6
|
+
import type { ZodShape } from '../types';
|
|
7
|
+
|
|
8
|
+
const isValidUrl = (value: string) => {
|
|
9
|
+
try {
|
|
10
|
+
return Object.values<string>(UrlProtocol).includes(new URL(value).protocol);
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const caseLinkSchema = z.object<ZodShape<WebitelCasesInputCaseLink>>({
|
|
17
|
+
url: z
|
|
18
|
+
.string()
|
|
19
|
+
.min(1)
|
|
20
|
+
.refine((value) => !value || isValidUrl(value), i18nIssue('url')),
|
|
21
|
+
name: z.string().optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export type CaseLink = z.infer<typeof caseLinkSchema>;
|
package/src/validations/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './caseClose/caseClose.validations';
|
|
|
14
14
|
export * from './caseCloseReason/caseCloseReason.validations';
|
|
15
15
|
export * from './caseCloseReasonGroup/caseCloseReasonGroup.validations';
|
|
16
16
|
export * from './caseExportOptions/caseExportOptions.validations';
|
|
17
|
+
export * from './caseLink/caseLink.validations';
|
|
17
18
|
export * from './casePriority/casePriority.validations';
|
|
18
19
|
export * from './caseService/caseService.validations';
|
|
19
20
|
export * from './caseServiceCatalog/caseServiceCatalog.validations';
|