@ikatec/digisac-api-sdk 4.2.0 → 4.4.0
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/dist/apis/contacts/ContactsApi.cjs +70 -0
- package/dist/apis/contacts/ContactsApi.cjs.map +1 -1
- package/dist/apis/contacts/ContactsApi.d.ts +51 -1
- package/dist/apis/contacts/ContactsApi.d.ts.map +1 -1
- package/dist/apis/contacts/ContactsApi.mjs +70 -0
- package/dist/apis/contacts/ContactsApi.mjs.map +1 -1
- package/dist/apis/contacts/types.d.ts +141 -1
- package/dist/apis/contacts/types.d.ts.map +1 -1
- package/dist/apis/dashboard/DashboardApi.cjs +36 -1
- package/dist/apis/dashboard/DashboardApi.cjs.map +1 -1
- package/dist/apis/dashboard/DashboardApi.d.ts +55 -2
- package/dist/apis/dashboard/DashboardApi.d.ts.map +1 -1
- package/dist/apis/dashboard/DashboardApi.mjs +36 -1
- package/dist/apis/dashboard/DashboardApi.mjs.map +1 -1
- package/dist/apis/dashboard/types.d.ts +71 -0
- package/dist/apis/dashboard/types.d.ts.map +1 -1
- package/dist/apis/services/ServicesApi.cjs +8 -0
- package/dist/apis/services/ServicesApi.cjs.map +1 -1
- package/dist/apis/services/ServicesApi.d.ts +7 -1
- package/dist/apis/services/ServicesApi.d.ts.map +1 -1
- package/dist/apis/services/ServicesApi.mjs +8 -0
- package/dist/apis/services/ServicesApi.mjs.map +1 -1
- package/dist/apis/services/types.d.ts +7 -0
- package/dist/apis/services/types.d.ts.map +1 -1
- package/dist/apis/tags/TagsApi.cjs +23 -0
- package/dist/apis/tags/TagsApi.cjs.map +1 -1
- package/dist/apis/tags/TagsApi.d.ts +19 -1
- package/dist/apis/tags/TagsApi.d.ts.map +1 -1
- package/dist/apis/tags/TagsApi.mjs +23 -0
- package/dist/apis/tags/TagsApi.mjs.map +1 -1
- package/dist/apis/tags/types.d.ts +8 -0
- package/dist/apis/tags/types.d.ts.map +1 -1
- package/dist/apis/users/UsersApi.cjs +77 -0
- package/dist/apis/users/UsersApi.cjs.map +1 -1
- package/dist/apis/users/UsersApi.d.ts +59 -1
- package/dist/apis/users/UsersApi.d.ts.map +1 -1
- package/dist/apis/users/UsersApi.mjs +77 -0
- package/dist/apis/users/UsersApi.mjs.map +1 -1
- package/dist/apis/users/types.d.ts +102 -0
- package/dist/apis/users/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/apis/acceptanceTerms/AcceptanceTermsApi.test.ts +1 -1
- package/src/apis/authHistory/AuthHistoryApi.test.ts +1 -1
- package/src/apis/campaigns/CampaignsApi.test.ts +1 -1
- package/src/apis/contacts/ContactsApi.test.ts +182 -5
- package/src/apis/contacts/ContactsApi.ts +128 -0
- package/src/apis/contacts/types.ts +154 -1
- package/src/apis/dashboard/DashboardApi.test-d.ts +49 -0
- package/src/apis/dashboard/DashboardApi.test.ts +197 -9
- package/src/apis/dashboard/DashboardApi.ts +130 -2
- package/src/apis/dashboard/types.ts +78 -0
- package/src/apis/messages/MessagesApi.test.ts +3 -3
- package/src/apis/payloads.test-d.ts +26 -0
- package/src/apis/queryFormat.test.ts +91 -3
- package/src/apis/serviceAccessManagement/ServiceAccessManagementApi.test.ts +2 -2
- package/src/apis/services/ServicesApi.test.ts +49 -0
- package/src/apis/services/ServicesApi.ts +10 -1
- package/src/apis/services/types.ts +9 -0
- package/src/apis/stickerUsers/StickerUsersApi.test.ts +1 -1
- package/src/apis/tags/TagsApi.test.ts +79 -0
- package/src/apis/tags/TagsApi.ts +57 -1
- package/src/apis/tags/types.ts +9 -0
- package/src/apis/terms/TermsApi.test.ts +1 -1
- package/src/apis/users/UsersApi.test.ts +228 -0
- package/src/apis/users/UsersApi.ts +152 -1
- package/src/apis/users/types.ts +121 -0
|
@@ -4,7 +4,7 @@ import type { Service } from '../services/types'
|
|
|
4
4
|
import type { Tag } from '../tags/types'
|
|
5
5
|
import type { Ticket } from '../tickets/types'
|
|
6
6
|
import type { User } from '../users/types'
|
|
7
|
-
import type { ListQuery } from '../../core/types'
|
|
7
|
+
import type { IncludeItem, ListQuery, WhereClause } from '../../core/types'
|
|
8
8
|
|
|
9
9
|
export type ContactStatus = 'online' | 'offline' | 'absent'
|
|
10
10
|
export type ContactOrigin = 'api' | 'app' | 'campaign' | 'driver' | 'import' | 'web'
|
|
@@ -204,3 +204,156 @@ export type TransferTicketPayload = {
|
|
|
204
204
|
kanbanCardSubject?: string
|
|
205
205
|
initialMessage?: string
|
|
206
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* One item of `POST /contacts/many`. The backend matches existing contacts by service and
|
|
210
|
+
* number (or e-mail) and updates them; the others are created. Items repeating a number of
|
|
211
|
+
* the same batch are dropped (the first one wins).
|
|
212
|
+
*/
|
|
213
|
+
export type CreateManyContactsItem = {
|
|
214
|
+
serviceId: string
|
|
215
|
+
name: string
|
|
216
|
+
/** Phone number with country code, or the e-mail address for e-mail connections. */
|
|
217
|
+
number: string
|
|
218
|
+
internalName?: string
|
|
219
|
+
personId?: string
|
|
220
|
+
defaultDepartmentId?: string
|
|
221
|
+
defaultUserId?: string
|
|
222
|
+
tagIds?: string[]
|
|
223
|
+
/** When `true`, the given `tagIds` are merged into the existing ones instead of replacing them. */
|
|
224
|
+
mergeRelations?: boolean
|
|
225
|
+
/**
|
|
226
|
+
* Only `data.number` is read, as a fallback when `number` is empty; the backend rebuilds
|
|
227
|
+
* `data` from the number.
|
|
228
|
+
*/
|
|
229
|
+
data?: {
|
|
230
|
+
number: string
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Selects a set of contacts by filter. Used by `DELETE /contacts/many` and by the tag
|
|
236
|
+
* attach/detach routes. The backend always scopes the filter to the current account.
|
|
237
|
+
*
|
|
238
|
+
* `where` is required on purpose: the backend applies the operation to every contact the
|
|
239
|
+
* filter matches, so an empty filter would reach every contact of the account.
|
|
240
|
+
*/
|
|
241
|
+
export type ContactsSelection = {
|
|
242
|
+
where: WhereClause<Contact>
|
|
243
|
+
include?: IncludeItem<Contact>[]
|
|
244
|
+
customFilter?: Record<string, unknown>
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* CSV layout of the contact export and import, as in `exportTemplate`:
|
|
249
|
+
* - `'colon'`: `;` as the column delimiter (any other value uses `,`);
|
|
250
|
+
* - `'google'`: phone columns as DDI + Phone ("Telefone") instead of DDI + Number.
|
|
251
|
+
*
|
|
252
|
+
* E-mail and webchat connections ignore it for the contact columns (Email / Email + Number).
|
|
253
|
+
*/
|
|
254
|
+
export type ContactsCsvType = 'colon' | 'google' | (string & {})
|
|
255
|
+
|
|
256
|
+
/** Body for `POST /contacts/export/csv`. */
|
|
257
|
+
export type ExportCsvPayload = Omit<ListQuery<Contact>, 'page' | 'perPage'> & {
|
|
258
|
+
serviceType: string
|
|
259
|
+
type: ContactsCsvType
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Body for `POST /contacts/import-contacts`. The CSV must be uploaded first (see `FilesApi`);
|
|
264
|
+
* the header line is checked against the template of the service type and language.
|
|
265
|
+
*/
|
|
266
|
+
export type ImportContactsPayload = {
|
|
267
|
+
service: Pick<Service, 'id' | 'accountId' | 'type'>
|
|
268
|
+
file: {
|
|
269
|
+
id: string
|
|
270
|
+
name: string
|
|
271
|
+
mimetype: string
|
|
272
|
+
/** Sent by the web app; the backend reads only `id`. */
|
|
273
|
+
urlToUpload?: string
|
|
274
|
+
}
|
|
275
|
+
csvHeaderLine: string[]
|
|
276
|
+
type: ContactsCsvType
|
|
277
|
+
tagsIds?: string[]
|
|
278
|
+
defaultDepartmentId?: string
|
|
279
|
+
defaultUserId?: string
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Body for `POST /contacts/:id/block`. Blocking a contact closes its open ticket. */
|
|
283
|
+
export type BlockContactPayload = {
|
|
284
|
+
block: boolean
|
|
285
|
+
byUserId?: string
|
|
286
|
+
description?: string
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Body for `POST /contacts/:id/sync`. */
|
|
290
|
+
export type SyncContactPayload = {
|
|
291
|
+
forceSync?: boolean
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export type ContactMediaType = 'media' | 'document' | 'link'
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Query for `GET /contacts/:id/media`. Cursor-based: pass the previous page's `nextCursor`
|
|
298
|
+
* as `where.cursor`.
|
|
299
|
+
*/
|
|
300
|
+
export type ContactMediaQuery = {
|
|
301
|
+
where?: {
|
|
302
|
+
/** `'media'` = images and videos. Omitted = images, videos and documents. */
|
|
303
|
+
type?: ContactMediaType
|
|
304
|
+
/** Case-insensitive filter by file name (ignored for links). */
|
|
305
|
+
name?: string
|
|
306
|
+
cursor?: string
|
|
307
|
+
}
|
|
308
|
+
perPage?: number
|
|
309
|
+
order?: [string, 'ASC' | 'DESC' | 'asc' | 'desc'][]
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* One item of `GET /contacts/:id/media`. When the user cannot see the message, the
|
|
314
|
+
* identifying fields come as `'UNAUTHORIZED_MESSAGE'` and `isAuthorized` is `false`.
|
|
315
|
+
*/
|
|
316
|
+
export type ContactMediaItem = {
|
|
317
|
+
id: string
|
|
318
|
+
/** ID of the message that carries the file or link. */
|
|
319
|
+
attachedId: string
|
|
320
|
+
/** File name; absent for links. */
|
|
321
|
+
name?: string | null
|
|
322
|
+
/** Only for files. */
|
|
323
|
+
mimetype?: string
|
|
324
|
+
createdAt: string
|
|
325
|
+
updatedAt: string
|
|
326
|
+
accountId: string
|
|
327
|
+
url: string | null
|
|
328
|
+
/** Only for files (filled for videos). */
|
|
329
|
+
previewUrl?: string | null
|
|
330
|
+
isAuthorized: boolean
|
|
331
|
+
/** Only for `type: 'link'`. */
|
|
332
|
+
links?: string[]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export type ContactMediaPage = {
|
|
336
|
+
data: ContactMediaItem[]
|
|
337
|
+
limit: number
|
|
338
|
+
hasNextPage: boolean
|
|
339
|
+
nextCursor: string | null
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Body for `POST /contacts/ticket/bulk-transfer`. */
|
|
343
|
+
export type BulkTransferTicketsPayload = {
|
|
344
|
+
departmentId?: string
|
|
345
|
+
/** `null` removes the assigned user. */
|
|
346
|
+
userId?: string | null
|
|
347
|
+
comments?: string
|
|
348
|
+
/** IDs of the tickets to transfer. Ignored when `allContactsSelected` is `true`. */
|
|
349
|
+
ticketsSelectedId?: string[]
|
|
350
|
+
allContactsSelected?: boolean
|
|
351
|
+
/** Contact filter used with `allContactsSelected`. */
|
|
352
|
+
query?: ListQuery<Contact>
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export type BulkTransferTicketsResult = {
|
|
356
|
+
message: string
|
|
357
|
+
/** IDs of the tickets actually transferred (the ones the target user can access). */
|
|
358
|
+
ticketsSelectedId: string[]
|
|
359
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { expectError, expectType } from 'tsd'
|
|
2
|
+
import type { ApiClient } from '../../core/ApiClient'
|
|
3
|
+
import { DashboardApi } from './DashboardApi'
|
|
4
|
+
import type {
|
|
5
|
+
DashboardDepartmentItem,
|
|
6
|
+
DashboardGrouped,
|
|
7
|
+
DashboardPeriodItem,
|
|
8
|
+
DashboardQuery,
|
|
9
|
+
DashboardUserItem,
|
|
10
|
+
} from './types'
|
|
11
|
+
|
|
12
|
+
declare const client: ApiClient
|
|
13
|
+
declare const filters: DashboardQuery
|
|
14
|
+
declare const flag: boolean
|
|
15
|
+
const dashboard = new DashboardApi(client)
|
|
16
|
+
|
|
17
|
+
// ── without `withTotals` (or `false`): the plain array ──────────────
|
|
18
|
+
|
|
19
|
+
expectType<Promise<DashboardPeriodItem[]>>(dashboard.getByPeriod())
|
|
20
|
+
expectType<Promise<DashboardPeriodItem[]>>(dashboard.getByPeriod({ grouping: 'weekly' }))
|
|
21
|
+
expectType<Promise<DashboardPeriodItem[]>>(dashboard.getByPeriod({ withTotals: false }))
|
|
22
|
+
expectType<Promise<DashboardPeriodItem[]>>(dashboard.getByPeriod(filters))
|
|
23
|
+
expectType<Promise<DashboardUserItem[]>>(dashboard.getByUser(filters))
|
|
24
|
+
expectType<Promise<DashboardDepartmentItem[]>>(dashboard.getByDepartment(filters))
|
|
25
|
+
|
|
26
|
+
// ── `withTotals: true`: `{ totals, items }` ─────────────────────────
|
|
27
|
+
|
|
28
|
+
expectType<Promise<DashboardGrouped<DashboardPeriodItem>>>(
|
|
29
|
+
dashboard.getByPeriod({ withTotals: true }),
|
|
30
|
+
)
|
|
31
|
+
expectType<Promise<DashboardGrouped<DashboardPeriodItem>>>(
|
|
32
|
+
dashboard.getByPeriod({ ...filters, withTotals: true }),
|
|
33
|
+
)
|
|
34
|
+
expectType<Promise<DashboardGrouped<DashboardUserItem>>>(dashboard.getByUser({ withTotals: true }))
|
|
35
|
+
expectType<Promise<DashboardGrouped<DashboardDepartmentItem>>>(
|
|
36
|
+
dashboard.getByDepartment({ withTotals: true }),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
// ── `withTotals` only known at runtime: the union ───────────────────
|
|
40
|
+
|
|
41
|
+
expectType<Promise<DashboardPeriodItem[] | DashboardGrouped<DashboardPeriodItem>>>(
|
|
42
|
+
dashboard.getByPeriod({ ...filters, withTotals: flag }),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// ── what the backend does not take ──────────────────────────────────
|
|
46
|
+
|
|
47
|
+
expectError(dashboard.getByPeriod({ grouping: 'quarterly' }))
|
|
48
|
+
expectError(dashboard.getByTopic({ withTotals: true }))
|
|
49
|
+
expectError(dashboard.getByService({ withTotals: true }))
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
2
|
import { DashboardApi } from './DashboardApi'
|
|
3
3
|
import type { ApiClient } from '../../core/ApiClient'
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
DashboardDepartmentItem,
|
|
6
|
+
DashboardGeneral,
|
|
7
|
+
DashboardPeriodItem,
|
|
8
|
+
DashboardQuery,
|
|
9
|
+
DashboardServiceItem,
|
|
10
|
+
DashboardTopicItem,
|
|
11
|
+
DashboardUserItem,
|
|
12
|
+
DashboardWithTotals,
|
|
13
|
+
} from './types'
|
|
5
14
|
|
|
6
15
|
// ── Mock client factory ──────────────────────────────────────────────
|
|
7
16
|
|
|
@@ -33,6 +42,37 @@ const general: DashboardGeneral = {
|
|
|
33
42
|
contactsCount: 4,
|
|
34
43
|
}
|
|
35
44
|
|
|
45
|
+
const periodItems: DashboardPeriodItem[] = [
|
|
46
|
+
{ name: '01/09/2026', sentMessagesCount: 4, receivedMessagesCount: 2, totalMessagesCount: 6 },
|
|
47
|
+
{ name: '02/09/2026', openedTicketsCount: 1, closedTicketsCount: 1, totalTicketsCount: 2 },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
const userItems: DashboardUserItem[] = [
|
|
51
|
+
{ name: 'Alice', sentMessagesCount: 7, closedTicketsCount: 3, ticketTopicsCount: 1 },
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const departmentItems: DashboardDepartmentItem[] = [
|
|
55
|
+
{ name: 'Suporte', openedTicketsCount: 2, waitingTime: 60, ticketTime: 600 },
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
const topicItems: DashboardTopicItem[] = [
|
|
59
|
+
{ name: 'Financeiro', openedTicketsCount: 1, waitingTimeAfterBot: 30, waitingTimeAvg: 45 },
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
const serviceItems: DashboardServiceItem[] = [
|
|
63
|
+
{ name: 'WhatsApp', receivedMessagesCount: 5, contactsCount: 4, ticketTopicsCount: 2 },
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
const filters: DashboardQuery = {
|
|
67
|
+
startPeriod: '2026-09-01T00:00:00.000Z',
|
|
68
|
+
endPeriod: '2026-09-07T23:59:59.999Z',
|
|
69
|
+
status: 'closed',
|
|
70
|
+
departmentId: ['d1'],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const queryString = (query: DashboardQuery & DashboardWithTotals) =>
|
|
74
|
+
`?query=${encodeURIComponent(JSON.stringify(query))}`
|
|
75
|
+
|
|
36
76
|
// ── Tests ────────────────────────────────────────────────────────────
|
|
37
77
|
|
|
38
78
|
describe('DashboardApi', () => {
|
|
@@ -59,15 +99,9 @@ describe('DashboardApi', () => {
|
|
|
59
99
|
|
|
60
100
|
it('sends the filters in the query string', async () => {
|
|
61
101
|
vi.mocked(client.get).mockResolvedValue(general)
|
|
62
|
-
|
|
63
|
-
startPeriod: '2026-09-01T00:00:00.000Z',
|
|
64
|
-
endPeriod: '2026-09-07T23:59:59.999Z',
|
|
65
|
-
status: 'closed' as const,
|
|
66
|
-
departmentId: ['d1'],
|
|
67
|
-
}
|
|
68
|
-
await api.getGeneral(query)
|
|
102
|
+
await api.getGeneral(filters)
|
|
69
103
|
expect(client.get).toHaveBeenCalledWith(
|
|
70
|
-
`/dashboard/general
|
|
104
|
+
`/dashboard/general${queryString(filters)}`,
|
|
71
105
|
undefined,
|
|
72
106
|
)
|
|
73
107
|
})
|
|
@@ -79,4 +113,158 @@ describe('DashboardApi', () => {
|
|
|
79
113
|
expect(client.get).toHaveBeenCalledWith('/dashboard/general', headers)
|
|
80
114
|
})
|
|
81
115
|
})
|
|
116
|
+
|
|
117
|
+
describe('getByPeriod', () => {
|
|
118
|
+
it('calls client.get with /dashboard/by-period and no query string', async () => {
|
|
119
|
+
vi.mocked(client.get).mockResolvedValue(periodItems)
|
|
120
|
+
const result = await api.getByPeriod()
|
|
121
|
+
expect(client.get).toHaveBeenCalledWith('/dashboard/by-period', undefined)
|
|
122
|
+
expect(result).toEqual(periodItems)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('sends the filters and the grouping in the query string', async () => {
|
|
126
|
+
vi.mocked(client.get).mockResolvedValue(periodItems)
|
|
127
|
+
const query: DashboardQuery = { ...filters, grouping: 'weekly' }
|
|
128
|
+
await api.getByPeriod(query)
|
|
129
|
+
expect(client.get).toHaveBeenCalledWith(
|
|
130
|
+
`/dashboard/by-period${queryString(query)}`,
|
|
131
|
+
undefined,
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('answers { totals, items } with withTotals: true', async () => {
|
|
136
|
+
const grouped = { totals: general, items: periodItems }
|
|
137
|
+
vi.mocked(client.get).mockResolvedValue(grouped)
|
|
138
|
+
const query = { ...filters, withTotals: true as const }
|
|
139
|
+
const result = await api.getByPeriod(query)
|
|
140
|
+
expect(client.get).toHaveBeenCalledWith(
|
|
141
|
+
`/dashboard/by-period${queryString(query)}`,
|
|
142
|
+
undefined,
|
|
143
|
+
)
|
|
144
|
+
expect(result).toEqual(grouped)
|
|
145
|
+
expect(result.totals.totalTicketsCount).toBe(5)
|
|
146
|
+
expect(result.items).toHaveLength(2)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('does not send withTotals: false (the backend reads it as truthy in the qs format)', async () => {
|
|
150
|
+
vi.mocked(client.get).mockResolvedValue(periodItems)
|
|
151
|
+
await api.getByPeriod({ ...filters, withTotals: false })
|
|
152
|
+
expect(client.get).toHaveBeenCalledWith(
|
|
153
|
+
`/dashboard/by-period${queryString(filters)}`,
|
|
154
|
+
undefined,
|
|
155
|
+
)
|
|
156
|
+
await api.withQueryFormat('qs').getByPeriod({ withTotals: false, grouping: 'weekly' })
|
|
157
|
+
expect(client.get).toHaveBeenLastCalledWith('/dashboard/by-period?grouping=weekly', undefined)
|
|
158
|
+
await api.getByPeriod({ withTotals: false })
|
|
159
|
+
expect(client.get).toHaveBeenLastCalledWith('/dashboard/by-period', undefined)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('forwards custom headers', async () => {
|
|
163
|
+
vi.mocked(client.get).mockResolvedValue(periodItems)
|
|
164
|
+
const headers = { 'X-Custom': 'h' }
|
|
165
|
+
await api.getByPeriod(undefined, headers)
|
|
166
|
+
expect(client.get).toHaveBeenCalledWith('/dashboard/by-period', headers)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('getByUser', () => {
|
|
171
|
+
it('calls client.get with /dashboard/by-user and the filters', async () => {
|
|
172
|
+
vi.mocked(client.get).mockResolvedValue(userItems)
|
|
173
|
+
const result = await api.getByUser(filters)
|
|
174
|
+
expect(client.get).toHaveBeenCalledWith(
|
|
175
|
+
`/dashboard/by-user${queryString(filters)}`,
|
|
176
|
+
undefined,
|
|
177
|
+
)
|
|
178
|
+
expect(result).toEqual(userItems)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('answers { totals, items } with withTotals: true', async () => {
|
|
182
|
+
const grouped = { totals: general, items: userItems }
|
|
183
|
+
vi.mocked(client.get).mockResolvedValue(grouped)
|
|
184
|
+
const result = await api.getByUser({ withTotals: true })
|
|
185
|
+
expect(client.get).toHaveBeenCalledWith(
|
|
186
|
+
`/dashboard/by-user${queryString({ withTotals: true })}`,
|
|
187
|
+
undefined,
|
|
188
|
+
)
|
|
189
|
+
expect(result.items[0]?.name).toBe('Alice')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('forwards custom headers', async () => {
|
|
193
|
+
vi.mocked(client.get).mockResolvedValue(userItems)
|
|
194
|
+
const headers = { 'X-Custom': 'h' }
|
|
195
|
+
await api.getByUser(undefined, headers)
|
|
196
|
+
expect(client.get).toHaveBeenCalledWith('/dashboard/by-user', headers)
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe('getByDepartment', () => {
|
|
201
|
+
it('calls client.get with /dashboard/by-department and the filters', async () => {
|
|
202
|
+
vi.mocked(client.get).mockResolvedValue(departmentItems)
|
|
203
|
+
const result = await api.getByDepartment(filters)
|
|
204
|
+
expect(client.get).toHaveBeenCalledWith(
|
|
205
|
+
`/dashboard/by-department${queryString(filters)}`,
|
|
206
|
+
undefined,
|
|
207
|
+
)
|
|
208
|
+
expect(result).toEqual(departmentItems)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('answers { totals, items } with withTotals: true', async () => {
|
|
212
|
+
const grouped = { totals: general, items: departmentItems }
|
|
213
|
+
vi.mocked(client.get).mockResolvedValue(grouped)
|
|
214
|
+
const result = await api.getByDepartment({ withTotals: true })
|
|
215
|
+
expect(result).toEqual(grouped)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('forwards custom headers', async () => {
|
|
219
|
+
vi.mocked(client.get).mockResolvedValue(departmentItems)
|
|
220
|
+
const headers = { 'X-Custom': 'h' }
|
|
221
|
+
await api.getByDepartment(undefined, headers)
|
|
222
|
+
expect(client.get).toHaveBeenCalledWith('/dashboard/by-department', headers)
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
describe('getByTopic', () => {
|
|
227
|
+
it('calls client.get with /dashboard/by-topic and the filters', async () => {
|
|
228
|
+
vi.mocked(client.get).mockResolvedValue(topicItems)
|
|
229
|
+
const result = await api.getByTopic(filters)
|
|
230
|
+
expect(client.get).toHaveBeenCalledWith(
|
|
231
|
+
`/dashboard/by-topic${queryString(filters)}`,
|
|
232
|
+
undefined,
|
|
233
|
+
)
|
|
234
|
+
expect(result).toEqual(topicItems)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('forwards custom headers', async () => {
|
|
238
|
+
vi.mocked(client.get).mockResolvedValue(topicItems)
|
|
239
|
+
const headers = { 'X-Custom': 'h' }
|
|
240
|
+
await api.getByTopic(undefined, headers)
|
|
241
|
+
expect(client.get).toHaveBeenCalledWith('/dashboard/by-topic', headers)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
describe('getByService', () => {
|
|
246
|
+
it('calls client.get with /dashboard/by-service and the filters', async () => {
|
|
247
|
+
vi.mocked(client.get).mockResolvedValue(serviceItems)
|
|
248
|
+
const result = await api.getByService(filters)
|
|
249
|
+
expect(client.get).toHaveBeenCalledWith(
|
|
250
|
+
`/dashboard/by-service${queryString(filters)}`,
|
|
251
|
+
undefined,
|
|
252
|
+
)
|
|
253
|
+
expect(result).toEqual(serviceItems)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('forwards custom headers', async () => {
|
|
257
|
+
vi.mocked(client.get).mockResolvedValue(serviceItems)
|
|
258
|
+
const headers = { 'X-Custom': 'h' }
|
|
259
|
+
await api.getByService(undefined, headers)
|
|
260
|
+
expect(client.get).toHaveBeenCalledWith('/dashboard/by-service', headers)
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
describe('error propagation', () => {
|
|
265
|
+
it('propagates errors from the grouped routes', async () => {
|
|
266
|
+
vi.mocked(client.get).mockRejectedValue(new Error('Unauthorized'))
|
|
267
|
+
await expect(api.getByPeriod()).rejects.toThrow('Unauthorized')
|
|
268
|
+
})
|
|
269
|
+
})
|
|
82
270
|
})
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { BaseApi } from '../../core/BaseApi'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
DashboardDepartmentItem,
|
|
4
|
+
DashboardGeneral,
|
|
5
|
+
DashboardGrouped,
|
|
6
|
+
DashboardPeriodItem,
|
|
7
|
+
DashboardQuery,
|
|
8
|
+
DashboardServiceItem,
|
|
9
|
+
DashboardTopicItem,
|
|
10
|
+
DashboardUserItem,
|
|
11
|
+
DashboardWithTotals,
|
|
12
|
+
} from './types'
|
|
3
13
|
|
|
4
14
|
/**
|
|
5
15
|
* Service statistics ("Estatísticas de atendimento"). The `/dashboard` routes only require an
|
|
6
|
-
* authenticated user (no permission check).
|
|
16
|
+
* authenticated user (no permission check). Every route takes the same {@link DashboardQuery}
|
|
17
|
+
* filters; the grouped routes answer one item per day, user, department, topic or connection.
|
|
7
18
|
*/
|
|
8
19
|
export class DashboardApi extends BaseApi {
|
|
9
20
|
public readonly basePath = '/dashboard'
|
|
@@ -15,4 +26,121 @@ export class DashboardApi extends BaseApi {
|
|
|
15
26
|
headers,
|
|
16
27
|
)
|
|
17
28
|
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The counts of each bucket of the period (`GET /dashboard/by-period`): one item per day by default,
|
|
32
|
+
* or per `grouping` (hour, week, month, year). With `withTotals: true` the answer is
|
|
33
|
+
* `{ totals, items }`.
|
|
34
|
+
*/
|
|
35
|
+
getByPeriod(
|
|
36
|
+
query: DashboardQuery & { withTotals: true },
|
|
37
|
+
headers?: Record<string, string>,
|
|
38
|
+
): Promise<DashboardGrouped<DashboardPeriodItem>>
|
|
39
|
+
getByPeriod(
|
|
40
|
+
query?: DashboardQuery & { withTotals?: false },
|
|
41
|
+
headers?: Record<string, string>,
|
|
42
|
+
): Promise<DashboardPeriodItem[]>
|
|
43
|
+
/** When `withTotals` is only known at runtime (e.g. a `boolean` variable). */
|
|
44
|
+
getByPeriod(
|
|
45
|
+
query?: DashboardQuery & DashboardWithTotals,
|
|
46
|
+
headers?: Record<string, string>,
|
|
47
|
+
): Promise<DashboardPeriodItem[] | DashboardGrouped<DashboardPeriodItem>>
|
|
48
|
+
getByPeriod(
|
|
49
|
+
query?: DashboardQuery & DashboardWithTotals,
|
|
50
|
+
headers?: Record<string, string>,
|
|
51
|
+
): Promise<DashboardPeriodItem[] | DashboardGrouped<DashboardPeriodItem>> {
|
|
52
|
+
return this.getGrouped<DashboardPeriodItem>('by-period', query, headers)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The counts of each user of the period (`GET /dashboard/by-user`). With `withTotals: true` the
|
|
57
|
+
* answer is `{ totals, items }`.
|
|
58
|
+
*/
|
|
59
|
+
getByUser(
|
|
60
|
+
query: DashboardQuery & { withTotals: true },
|
|
61
|
+
headers?: Record<string, string>,
|
|
62
|
+
): Promise<DashboardGrouped<DashboardUserItem>>
|
|
63
|
+
getByUser(
|
|
64
|
+
query?: DashboardQuery & { withTotals?: false },
|
|
65
|
+
headers?: Record<string, string>,
|
|
66
|
+
): Promise<DashboardUserItem[]>
|
|
67
|
+
/** When `withTotals` is only known at runtime (e.g. a `boolean` variable). */
|
|
68
|
+
getByUser(
|
|
69
|
+
query?: DashboardQuery & DashboardWithTotals,
|
|
70
|
+
headers?: Record<string, string>,
|
|
71
|
+
): Promise<DashboardUserItem[] | DashboardGrouped<DashboardUserItem>>
|
|
72
|
+
getByUser(
|
|
73
|
+
query?: DashboardQuery & DashboardWithTotals,
|
|
74
|
+
headers?: Record<string, string>,
|
|
75
|
+
): Promise<DashboardUserItem[] | DashboardGrouped<DashboardUserItem>> {
|
|
76
|
+
return this.getGrouped<DashboardUserItem>('by-user', query, headers)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The counts of each department of the period (`GET /dashboard/by-department`). With
|
|
81
|
+
* `withTotals: true` the answer is `{ totals, items }`.
|
|
82
|
+
*/
|
|
83
|
+
getByDepartment(
|
|
84
|
+
query: DashboardQuery & { withTotals: true },
|
|
85
|
+
headers?: Record<string, string>,
|
|
86
|
+
): Promise<DashboardGrouped<DashboardDepartmentItem>>
|
|
87
|
+
getByDepartment(
|
|
88
|
+
query?: DashboardQuery & { withTotals?: false },
|
|
89
|
+
headers?: Record<string, string>,
|
|
90
|
+
): Promise<DashboardDepartmentItem[]>
|
|
91
|
+
/** When `withTotals` is only known at runtime (e.g. a `boolean` variable). */
|
|
92
|
+
getByDepartment(
|
|
93
|
+
query?: DashboardQuery & DashboardWithTotals,
|
|
94
|
+
headers?: Record<string, string>,
|
|
95
|
+
): Promise<DashboardDepartmentItem[] | DashboardGrouped<DashboardDepartmentItem>>
|
|
96
|
+
getByDepartment(
|
|
97
|
+
query?: DashboardQuery & DashboardWithTotals,
|
|
98
|
+
headers?: Record<string, string>,
|
|
99
|
+
): Promise<DashboardDepartmentItem[] | DashboardGrouped<DashboardDepartmentItem>> {
|
|
100
|
+
return this.getGrouped<DashboardDepartmentItem>('by-department', query, headers)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The counts of each ticket topic of the period (`GET /dashboard/by-topic`). Does not take
|
|
105
|
+
* `withTotals` (the backend ignores it).
|
|
106
|
+
*/
|
|
107
|
+
getByTopic(
|
|
108
|
+
query?: DashboardQuery,
|
|
109
|
+
headers?: Record<string, string>,
|
|
110
|
+
): Promise<DashboardTopicItem[]> {
|
|
111
|
+
return this.client.get<DashboardTopicItem[]>(
|
|
112
|
+
`${this.basePath}/by-topic${this.toQueryString(query)}`,
|
|
113
|
+
headers,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The counts of each connection of the period (`GET /dashboard/by-service`). Does not take
|
|
119
|
+
* `withTotals` (the backend ignores it).
|
|
120
|
+
*/
|
|
121
|
+
getByService(
|
|
122
|
+
query?: DashboardQuery,
|
|
123
|
+
headers?: Record<string, string>,
|
|
124
|
+
): Promise<DashboardServiceItem[]> {
|
|
125
|
+
return this.client.get<DashboardServiceItem[]>(
|
|
126
|
+
`${this.basePath}/by-service${this.toQueryString(query)}`,
|
|
127
|
+
headers,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* `withTotals` is only sent when `true`: the backend reads it with a truthiness check, so in the
|
|
133
|
+
* `qs` format a serialized `withTotals=false` (the string `'false'`) would wrap the answer anyway.
|
|
134
|
+
*/
|
|
135
|
+
private getGrouped<TItem>(
|
|
136
|
+
route: string,
|
|
137
|
+
query?: DashboardQuery & DashboardWithTotals,
|
|
138
|
+
headers?: Record<string, string>,
|
|
139
|
+
): Promise<TItem[] | DashboardGrouped<TItem>> {
|
|
140
|
+
const { withTotals, ...filters } = query ?? {}
|
|
141
|
+
return this.client.get<TItem[] | DashboardGrouped<TItem>>(
|
|
142
|
+
`${this.basePath}/${route}${this.toQueryString(withTotals ? { ...filters, withTotals: true } : filters)}`,
|
|
143
|
+
headers,
|
|
144
|
+
)
|
|
145
|
+
}
|
|
18
146
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Granularity of the by-period items (`grouping`). Default: `daily`. Whatever the granularity, the
|
|
3
|
+
* `name` of each item is its day (`dd/MM/yyyy`), so with `hourly` the items of one day share the same
|
|
4
|
+
* `name` (they come in chronological order).
|
|
5
|
+
*/
|
|
6
|
+
export type DashboardGrouping = 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly'
|
|
7
|
+
|
|
1
8
|
/**
|
|
2
9
|
* Filters of the service statistics (`/dashboard`). Every field is optional; the period defaults to
|
|
3
10
|
* the last 7 days.
|
|
@@ -35,8 +42,21 @@ export type DashboardQuery = {
|
|
|
35
42
|
serviceId?: string | string[]
|
|
36
43
|
tagId?: string | string[]
|
|
37
44
|
ticketTopicId?: string | string[]
|
|
45
|
+
/**
|
|
46
|
+
* Granularity of the `getByPeriod` items (the other routes ignore it). Default: `daily`. See
|
|
47
|
+
* {@link DashboardGrouping} for the `name` of the items.
|
|
48
|
+
*/
|
|
49
|
+
grouping?: DashboardGrouping
|
|
38
50
|
}
|
|
39
51
|
|
|
52
|
+
/**
|
|
53
|
+
* `withTotals: true` makes `getByPeriod`, `getByUser` and `getByDepartment` answer `{ totals, items }`,
|
|
54
|
+
* where `totals` are the `getGeneral` counts of the same filters (`getByTopic` and `getByService` do
|
|
55
|
+
* not take it). Kept out of `DashboardQuery` so a query without it keeps the plain array overload.
|
|
56
|
+
* Only sent to the backend when `true`.
|
|
57
|
+
*/
|
|
58
|
+
export type DashboardWithTotals = { withTotals?: boolean }
|
|
59
|
+
|
|
40
60
|
/** `GET /dashboard/general` — counts of the period. Times are in seconds. */
|
|
41
61
|
export type DashboardGeneral = {
|
|
42
62
|
sentMessagesCount: number
|
|
@@ -51,3 +71,61 @@ export type DashboardGeneral = {
|
|
|
51
71
|
ticketTime: number
|
|
52
72
|
contactsCount: number
|
|
53
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Counts shared by the grouped routes (`by-period`, `by-user`, `by-department`, `by-topic`,
|
|
77
|
+
* `by-service`). The backend builds each item from the rows it found, so a count is ABSENT (not 0)
|
|
78
|
+
* when the group has no row for it: read them with a default. Times are in seconds.
|
|
79
|
+
*/
|
|
80
|
+
export type DashboardGroupCounts = {
|
|
81
|
+
sentMessagesCount?: number
|
|
82
|
+
receivedMessagesCount?: number
|
|
83
|
+
openedTicketsCount?: number
|
|
84
|
+
closedTicketsCount?: number
|
|
85
|
+
waitingTime?: number
|
|
86
|
+
ticketTime?: number
|
|
87
|
+
contactsCount?: number
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** `GET /dashboard/by-period` — the counts of one day of the period (`name` as `dd/MM/yyyy`). */
|
|
91
|
+
export type DashboardPeriodItem = DashboardGroupCounts & {
|
|
92
|
+
name: string
|
|
93
|
+
totalMessagesCount?: number
|
|
94
|
+
totalTicketsCount?: number
|
|
95
|
+
waitingTimeAfterBot?: number
|
|
96
|
+
waitingTimeAvg?: number
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `GET /dashboard/by-user` — the counts of one user (`name` is the user's name). */
|
|
100
|
+
export type DashboardUserItem = DashboardGroupCounts & {
|
|
101
|
+
name: string
|
|
102
|
+
ticketTopicsCount?: number
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** `GET /dashboard/by-department` — the counts of one department (`name` is its name). */
|
|
106
|
+
export type DashboardDepartmentItem = DashboardGroupCounts & {
|
|
107
|
+
name: string
|
|
108
|
+
ticketTopicsCount?: number
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** `GET /dashboard/by-topic` — the counts of one ticket topic (`name` is the topic's name). */
|
|
112
|
+
export type DashboardTopicItem = DashboardGroupCounts & {
|
|
113
|
+
name: string
|
|
114
|
+
waitingTimeAfterBot?: number
|
|
115
|
+
waitingTimeAvg?: number
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** `GET /dashboard/by-service` — the counts of one connection (`name` is the connection's name). */
|
|
119
|
+
export type DashboardServiceItem = DashboardGroupCounts & {
|
|
120
|
+
name: string
|
|
121
|
+
waitingTimeAfterBot?: number
|
|
122
|
+
waitingTimeAvg?: number
|
|
123
|
+
ticketTopicsCount?: number
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Answer of `getByPeriod`, `getByUser` and `getByDepartment` with `withTotals: true`. */
|
|
127
|
+
export type DashboardGrouped<TItem> = {
|
|
128
|
+
/** The `getGeneral` counts of the same filters. */
|
|
129
|
+
totals: DashboardGeneral
|
|
130
|
+
items: TItem[]
|
|
131
|
+
}
|