@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.
Files changed (66) hide show
  1. package/dist/apis/contacts/ContactsApi.cjs +70 -0
  2. package/dist/apis/contacts/ContactsApi.cjs.map +1 -1
  3. package/dist/apis/contacts/ContactsApi.d.ts +51 -1
  4. package/dist/apis/contacts/ContactsApi.d.ts.map +1 -1
  5. package/dist/apis/contacts/ContactsApi.mjs +70 -0
  6. package/dist/apis/contacts/ContactsApi.mjs.map +1 -1
  7. package/dist/apis/contacts/types.d.ts +141 -1
  8. package/dist/apis/contacts/types.d.ts.map +1 -1
  9. package/dist/apis/dashboard/DashboardApi.cjs +36 -1
  10. package/dist/apis/dashboard/DashboardApi.cjs.map +1 -1
  11. package/dist/apis/dashboard/DashboardApi.d.ts +55 -2
  12. package/dist/apis/dashboard/DashboardApi.d.ts.map +1 -1
  13. package/dist/apis/dashboard/DashboardApi.mjs +36 -1
  14. package/dist/apis/dashboard/DashboardApi.mjs.map +1 -1
  15. package/dist/apis/dashboard/types.d.ts +71 -0
  16. package/dist/apis/dashboard/types.d.ts.map +1 -1
  17. package/dist/apis/services/ServicesApi.cjs +8 -0
  18. package/dist/apis/services/ServicesApi.cjs.map +1 -1
  19. package/dist/apis/services/ServicesApi.d.ts +7 -1
  20. package/dist/apis/services/ServicesApi.d.ts.map +1 -1
  21. package/dist/apis/services/ServicesApi.mjs +8 -0
  22. package/dist/apis/services/ServicesApi.mjs.map +1 -1
  23. package/dist/apis/services/types.d.ts +7 -0
  24. package/dist/apis/services/types.d.ts.map +1 -1
  25. package/dist/apis/tags/TagsApi.cjs +23 -0
  26. package/dist/apis/tags/TagsApi.cjs.map +1 -1
  27. package/dist/apis/tags/TagsApi.d.ts +19 -1
  28. package/dist/apis/tags/TagsApi.d.ts.map +1 -1
  29. package/dist/apis/tags/TagsApi.mjs +23 -0
  30. package/dist/apis/tags/TagsApi.mjs.map +1 -1
  31. package/dist/apis/tags/types.d.ts +8 -0
  32. package/dist/apis/tags/types.d.ts.map +1 -1
  33. package/dist/apis/users/UsersApi.cjs +77 -0
  34. package/dist/apis/users/UsersApi.cjs.map +1 -1
  35. package/dist/apis/users/UsersApi.d.ts +59 -1
  36. package/dist/apis/users/UsersApi.d.ts.map +1 -1
  37. package/dist/apis/users/UsersApi.mjs +77 -0
  38. package/dist/apis/users/UsersApi.mjs.map +1 -1
  39. package/dist/apis/users/types.d.ts +102 -0
  40. package/dist/apis/users/types.d.ts.map +1 -1
  41. package/package.json +1 -1
  42. package/src/apis/acceptanceTerms/AcceptanceTermsApi.test.ts +1 -1
  43. package/src/apis/authHistory/AuthHistoryApi.test.ts +1 -1
  44. package/src/apis/campaigns/CampaignsApi.test.ts +1 -1
  45. package/src/apis/contacts/ContactsApi.test.ts +182 -5
  46. package/src/apis/contacts/ContactsApi.ts +128 -0
  47. package/src/apis/contacts/types.ts +154 -1
  48. package/src/apis/dashboard/DashboardApi.test-d.ts +49 -0
  49. package/src/apis/dashboard/DashboardApi.test.ts +197 -9
  50. package/src/apis/dashboard/DashboardApi.ts +130 -2
  51. package/src/apis/dashboard/types.ts +78 -0
  52. package/src/apis/messages/MessagesApi.test.ts +3 -3
  53. package/src/apis/payloads.test-d.ts +26 -0
  54. package/src/apis/queryFormat.test.ts +91 -3
  55. package/src/apis/serviceAccessManagement/ServiceAccessManagementApi.test.ts +2 -2
  56. package/src/apis/services/ServicesApi.test.ts +49 -0
  57. package/src/apis/services/ServicesApi.ts +10 -1
  58. package/src/apis/services/types.ts +9 -0
  59. package/src/apis/stickerUsers/StickerUsersApi.test.ts +1 -1
  60. package/src/apis/tags/TagsApi.test.ts +79 -0
  61. package/src/apis/tags/TagsApi.ts +57 -1
  62. package/src/apis/tags/types.ts +9 -0
  63. package/src/apis/terms/TermsApi.test.ts +1 -1
  64. package/src/apis/users/UsersApi.test.ts +228 -0
  65. package/src/apis/users/UsersApi.ts +152 -1
  66. 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 { DashboardGeneral } from './types'
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
- const query = {
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?query=${encodeURIComponent(JSON.stringify(query))}`,
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 { DashboardGeneral, DashboardQuery } from './types'
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
+ }