@ikatec/digisac-api-sdk 4.4.0 → 4.5.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 (76) hide show
  1. package/dist/apis/cards/CardsApi.cjs +70 -0
  2. package/dist/apis/cards/CardsApi.cjs.map +1 -1
  3. package/dist/apis/cards/CardsApi.d.ts +53 -1
  4. package/dist/apis/cards/CardsApi.d.ts.map +1 -1
  5. package/dist/apis/cards/CardsApi.mjs +70 -0
  6. package/dist/apis/cards/CardsApi.mjs.map +1 -1
  7. package/dist/apis/cards/types.d.ts +84 -0
  8. package/dist/apis/cards/types.d.ts.map +1 -1
  9. package/dist/apis/kanbanBoards/types.d.ts +33 -4
  10. package/dist/apis/kanbanBoards/types.d.ts.map +1 -1
  11. package/dist/apis/kanbanCards/KanbanCardsApi.cjs +8 -0
  12. package/dist/apis/kanbanCards/KanbanCardsApi.cjs.map +1 -1
  13. package/dist/apis/kanbanCards/KanbanCardsApi.d.ts +7 -1
  14. package/dist/apis/kanbanCards/KanbanCardsApi.d.ts.map +1 -1
  15. package/dist/apis/kanbanCards/KanbanCardsApi.mjs +8 -0
  16. package/dist/apis/kanbanCards/KanbanCardsApi.mjs.map +1 -1
  17. package/dist/apis/kanbanCards/types.d.ts +17 -5
  18. package/dist/apis/kanbanCards/types.d.ts.map +1 -1
  19. package/dist/apis/kanbanColumns/KanbanColumnsApi.cjs +9 -0
  20. package/dist/apis/kanbanColumns/KanbanColumnsApi.cjs.map +1 -1
  21. package/dist/apis/kanbanColumns/KanbanColumnsApi.d.ts +8 -1
  22. package/dist/apis/kanbanColumns/KanbanColumnsApi.d.ts.map +1 -1
  23. package/dist/apis/kanbanColumns/KanbanColumnsApi.mjs +9 -0
  24. package/dist/apis/kanbanColumns/KanbanColumnsApi.mjs.map +1 -1
  25. package/dist/apis/kanbanColumns/types.d.ts +14 -3
  26. package/dist/apis/kanbanColumns/types.d.ts.map +1 -1
  27. package/dist/apis/messages/MessagesApi.cjs +4 -1
  28. package/dist/apis/messages/MessagesApi.cjs.map +1 -1
  29. package/dist/apis/messages/MessagesApi.d.ts +5 -2
  30. package/dist/apis/messages/MessagesApi.d.ts.map +1 -1
  31. package/dist/apis/messages/MessagesApi.mjs +4 -1
  32. package/dist/apis/messages/MessagesApi.mjs.map +1 -1
  33. package/dist/apis/messages/types.d.ts +13 -0
  34. package/dist/apis/messages/types.d.ts.map +1 -1
  35. package/dist/apis/pipeline/PipelineApi.cjs +133 -0
  36. package/dist/apis/pipeline/PipelineApi.cjs.map +1 -1
  37. package/dist/apis/pipeline/PipelineApi.d.ts +102 -1
  38. package/dist/apis/pipeline/PipelineApi.d.ts.map +1 -1
  39. package/dist/apis/pipeline/PipelineApi.mjs +133 -0
  40. package/dist/apis/pipeline/PipelineApi.mjs.map +1 -1
  41. package/dist/apis/pipeline/types.d.ts +164 -0
  42. package/dist/apis/pipeline/types.d.ts.map +1 -1
  43. package/dist/apis/tags/TagsApi.cjs +3 -2
  44. package/dist/apis/tags/TagsApi.cjs.map +1 -1
  45. package/dist/apis/tags/TagsApi.d.ts +6 -5
  46. package/dist/apis/tags/TagsApi.d.ts.map +1 -1
  47. package/dist/apis/tags/TagsApi.mjs +3 -2
  48. package/dist/apis/tags/TagsApi.mjs.map +1 -1
  49. package/dist/apis/tags/types.d.ts +5 -0
  50. package/dist/apis/tags/types.d.ts.map +1 -1
  51. package/dist/apis/users/types.d.ts +21 -3
  52. package/dist/apis/users/types.d.ts.map +1 -1
  53. package/package.json +1 -1
  54. package/src/apis/cards/CardsApi.test.ts +128 -0
  55. package/src/apis/cards/CardsApi.ts +124 -1
  56. package/src/apis/cards/types.ts +81 -0
  57. package/src/apis/kanbanBoards/types.ts +36 -4
  58. package/src/apis/kanbanCards/KanbanCardsApi.test.ts +46 -0
  59. package/src/apis/kanbanCards/KanbanCardsApi.ts +23 -1
  60. package/src/apis/kanbanCards/types.ts +18 -5
  61. package/src/apis/kanbanColumns/KanbanColumnsApi.test.ts +46 -0
  62. package/src/apis/kanbanColumns/KanbanColumnsApi.ts +24 -1
  63. package/src/apis/kanbanColumns/types.ts +15 -3
  64. package/src/apis/me/MeApi.test.ts +3 -3
  65. package/src/apis/messages/MessagesApi.test.ts +11 -0
  66. package/src/apis/messages/MessagesApi.ts +6 -3
  67. package/src/apis/messages/types.ts +13 -0
  68. package/src/apis/payloads.test-d.ts +30 -0
  69. package/src/apis/pipeline/PipelineApi.test.ts +193 -0
  70. package/src/apis/pipeline/PipelineApi.ts +274 -1
  71. package/src/apis/pipeline/types.ts +162 -0
  72. package/src/apis/tags/TagsApi.test.ts +5 -0
  73. package/src/apis/tags/TagsApi.ts +8 -6
  74. package/src/apis/tags/types.ts +6 -0
  75. package/src/apis/users/UsersApi.test.ts +3 -3
  76. package/src/apis/users/types.ts +23 -3
@@ -0,0 +1,128 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { ApiClient } from '../../core/ApiClient'
3
+ import { CardsApi } from './CardsApi'
4
+
5
+ // ── Mock client factory ──────────────────────────────────────────────
6
+
7
+ function createMockClient(): ApiClient {
8
+ return {
9
+ setAccessToken: vi.fn().mockReturnThis(),
10
+ request: vi.fn(),
11
+ get: vi.fn(),
12
+ post: vi.fn(),
13
+ put: vi.fn(),
14
+ patch: vi.fn(),
15
+ delete: vi.fn(),
16
+ }
17
+ }
18
+
19
+ const headers = { 'X-Custom': 'value' }
20
+
21
+ // ── Tests ────────────────────────────────────────────────────────────
22
+
23
+ describe('CardsApi', () => {
24
+ let client: ReturnType<typeof createMockClient>
25
+ let api: CardsApi
26
+
27
+ beforeEach(() => {
28
+ client = createMockClient()
29
+ api = new CardsApi(client)
30
+ })
31
+
32
+ describe('bulkMove', () => {
33
+ it('calls client.put with /cards/bulk-move and body', async () => {
34
+ vi.mocked(client.put).mockResolvedValue({ movedCount: 2, ignoredCount: 0 })
35
+ const body = {
36
+ ids: ['c1', 'c2'],
37
+ originPipelineStageId: 's1',
38
+ pipelineId: 'p1',
39
+ pipelineStageId: 's2',
40
+ }
41
+ const result = await api.bulkMove(body, headers)
42
+ expect(client.put).toHaveBeenCalledWith('/cards/bulk-move', body, headers)
43
+ expect(result).toEqual({ movedCount: 2, ignoredCount: 0 })
44
+ })
45
+ })
46
+
47
+ describe('updateOrder', () => {
48
+ it('calls client.put with /cards/order/:id and the order', async () => {
49
+ await api.updateOrder('c1', 3)
50
+ expect(client.put).toHaveBeenCalledWith('/cards/order/c1', { order: 3 }, undefined)
51
+ })
52
+ })
53
+
54
+ describe('products', () => {
55
+ it('saveProduct calls client.post with /cards/product and body', async () => {
56
+ vi.mocked(client.post).mockResolvedValue({ message: 'Operação realizada com sucesso.' })
57
+ const body = { cardId: 'c1', name: 'Plano', ammount: 2, value: 99.9 }
58
+ const result = await api.saveProduct(body)
59
+ expect(client.post).toHaveBeenCalledWith('/cards/product', body, undefined)
60
+ expect(result).toEqual({ message: 'Operação realizada com sucesso.' })
61
+ })
62
+
63
+ it('deleteProduct calls client.delete with /cards/product/:id', async () => {
64
+ await api.deleteProduct('pr1', headers)
65
+ expect(client.delete).toHaveBeenCalledWith('/cards/product/pr1', headers)
66
+ })
67
+ })
68
+
69
+ describe('comments', () => {
70
+ it('saveComment calls client.post with /cards/comment and body', async () => {
71
+ const body = { cardId: 'c1', comment: 'Ligar amanhã' }
72
+ await api.saveComment(body)
73
+ expect(client.post).toHaveBeenCalledWith('/cards/comment', body, undefined)
74
+ })
75
+
76
+ it('saveComment edits by id', async () => {
77
+ const body = { id: 'cm1', comment: 'Ligar hoje', removedFileIds: ['f1'] }
78
+ await api.saveComment(body, headers)
79
+ expect(client.post).toHaveBeenCalledWith('/cards/comment', body, headers)
80
+ })
81
+
82
+ it('deleteComment calls client.delete with /cards/comment/:id', async () => {
83
+ await api.deleteComment('cm1')
84
+ expect(client.delete).toHaveBeenCalledWith('/cards/comment/cm1', undefined)
85
+ })
86
+ })
87
+
88
+ describe('reads', () => {
89
+ it('getOwners calls client.post with /cards/owners and no body', async () => {
90
+ vi.mocked(client.post).mockResolvedValue([{ id: 'u1', name: 'Ana', disabled: false }])
91
+ const result = await api.getOwners()
92
+ expect(client.post).toHaveBeenCalledWith('/cards/owners', undefined, undefined)
93
+ expect(result).toEqual([{ id: 'u1', name: 'Ana', disabled: false }])
94
+ })
95
+
96
+ it('getCards calls client.post with /cards/getCards and the query', async () => {
97
+ const body = {
98
+ where: { accountId: 'a1', pipelineStageId: 's1' },
99
+ attributes: ['id', 'title'] as ('id' | 'title')[],
100
+ order: [['contact.name', 'ASC']] as ['contact.name', 'ASC'][],
101
+ limit: 20,
102
+ }
103
+ await api.getCards(body)
104
+ expect(client.post).toHaveBeenCalledWith('/cards/getCards', body, undefined)
105
+ })
106
+
107
+ it('getContactTotals calls client.post with /cards/contact-totals/:contactId', async () => {
108
+ vi.mocked(client.post).mockResolvedValue({ totalOpen: 1 })
109
+ const result = await api.getContactTotals('ct1', headers)
110
+ expect(client.post).toHaveBeenCalledWith('/cards/contact-totals/ct1', undefined, headers)
111
+ expect(result).toEqual({ totalOpen: 1 })
112
+ })
113
+ })
114
+
115
+ describe('error propagation', () => {
116
+ it('propagates errors from bulkMove', async () => {
117
+ vi.mocked(client.put).mockRejectedValue(new Error('At least one card id is required.'))
118
+ await expect(
119
+ api.bulkMove({
120
+ ids: [],
121
+ originPipelineStageId: 's1',
122
+ pipelineId: 'p1',
123
+ pipelineStageId: 's2',
124
+ }),
125
+ ).rejects.toThrow('At least one card id is required.')
126
+ })
127
+ })
128
+ })
@@ -1,9 +1,132 @@
1
1
  import type { ApiClient } from '../../core/ApiClient'
2
2
  import { BaseCrudApi } from '../../core/BaseCrudApi'
3
- import type { Card, CreateCardPayload, UpdateCardPayload } from './types'
3
+ import type {
4
+ BulkMoveCardsPayload,
5
+ BulkMoveCardsResult,
6
+ Card,
7
+ CardContactTotals,
8
+ CardOperationResult,
9
+ CardOwner,
10
+ CreateCardPayload,
11
+ GetCardsPayload,
12
+ SaveCardCommentPayload,
13
+ SaveCardProductPayload,
14
+ UpdateCardPayload,
15
+ } from './types'
4
16
 
5
17
  export class CardsApi extends BaseCrudApi<Card, CreateCardPayload, UpdateCardPayload> {
6
18
  constructor(client: ApiClient) {
7
19
  super(client, '/cards')
8
20
  }
21
+
22
+ /**
23
+ * Moves several cards of one stage to another stage (of the same or another pipeline),
24
+ * clearing their status/reason unless given. Cards outside the account or not in
25
+ * `originPipelineStageId` are skipped and counted in `ignoredCount`.
26
+ * @permissions pipelines.opportunity.update
27
+ */
28
+ bulkMove(
29
+ body: BulkMoveCardsPayload,
30
+ headers?: Record<string, string>,
31
+ ): Promise<BulkMoveCardsResult> {
32
+ return this.client.put<BulkMoveCardsResult, BulkMoveCardsPayload>(
33
+ `${this.basePath}/bulk-move`,
34
+ body,
35
+ headers,
36
+ )
37
+ }
38
+
39
+ /**
40
+ * Moves a card to the `order` position of its stage, shifting the cards in between. Resolves
41
+ * to the card as stored. The backend refuses `0` (400 `required`), although the first card of
42
+ * a stage has order 0.
43
+ * @permissions pipelines.opportunity.update
44
+ */
45
+ updateOrder(id: string, order: number, headers?: Record<string, string>): Promise<Card> {
46
+ return this.client.put<Card, { order: number }>(
47
+ `${this.basePath}/order/${id}`,
48
+ { order },
49
+ headers,
50
+ )
51
+ }
52
+
53
+ /**
54
+ * Adds a product to a card, or updates the product of `body.id`.
55
+ * @permissions pipelines.opportunity.update
56
+ */
57
+ saveProduct(
58
+ body: SaveCardProductPayload,
59
+ headers?: Record<string, string>,
60
+ ): Promise<CardOperationResult> {
61
+ return this.client.post<CardOperationResult, SaveCardProductPayload>(
62
+ `${this.basePath}/product`,
63
+ body,
64
+ headers,
65
+ )
66
+ }
67
+
68
+ /**
69
+ * Deletes a product of a card. An unknown id also resolves with 200, with the message
70
+ * `'Produto não encontrado'`.
71
+ * @permissions pipelines.opportunity.update
72
+ */
73
+ deleteProduct(id: string, headers?: Record<string, string>): Promise<CardOperationResult> {
74
+ return this.client.delete<CardOperationResult>(`${this.basePath}/product/${id}`, headers)
75
+ }
76
+
77
+ /**
78
+ * Adds a comment to a card, or edits the comment of `body.id`. Resolves to a message, not to
79
+ * the comment — read it back through `getById(cardId, { include: ['comments'] })`. A card or
80
+ * comment of another account fails with 404.
81
+ * @permissions pipelines.opportunity.update
82
+ */
83
+ saveComment(
84
+ body: SaveCardCommentPayload,
85
+ headers?: Record<string, string>,
86
+ ): Promise<CardOperationResult> {
87
+ return this.client.post<CardOperationResult, SaveCardCommentPayload>(
88
+ `${this.basePath}/comment`,
89
+ body,
90
+ headers,
91
+ )
92
+ }
93
+
94
+ /**
95
+ * Deletes a comment of a card. Resolves with no content; unknown → 404.
96
+ * @permissions pipelines.opportunity.update
97
+ */
98
+ deleteComment(id: string, headers?: Record<string, string>): Promise<void> {
99
+ return this.client.delete<void>(`${this.basePath}/comment/${id}`, headers)
100
+ }
101
+
102
+ /**
103
+ * Lists the users of the account that can own a card, sorted by name.
104
+ * @permissions pipelines.opportunity.view
105
+ */
106
+ getOwners(headers?: Record<string, string>): Promise<CardOwner[]> {
107
+ return this.client.post<CardOwner[]>(`${this.basePath}/owners`, undefined, headers)
108
+ }
109
+
110
+ /**
111
+ * Lists cards with the board query of the UI. Resolves to a plain array (no pagination).
112
+ * @permissions pipelines.opportunity.view
113
+ */
114
+ getCards(body: GetCardsPayload, headers?: Record<string, string>): Promise<Card[]> {
115
+ return this.client.post<Card[], GetCardsPayload>(`${this.basePath}/getCards`, body, headers)
116
+ }
117
+
118
+ /**
119
+ * Counts the open cards of a contact visible to the caller.
120
+ * @permissions pipelines.opportunity.view
121
+ */
122
+ getContactTotals(
123
+ contactId: string,
124
+ headers?: Record<string, string>,
125
+ ): Promise<CardContactTotals> {
126
+ return this.client.post<CardContactTotals>(
127
+ `${this.basePath}/contact-totals/${contactId}`,
128
+ undefined,
129
+ headers,
130
+ )
131
+ }
9
132
  }
@@ -1,3 +1,4 @@
1
+ import type { ListQuery, WhereClause } from '../../core/types'
1
2
  import type { Contact } from '../contacts/types'
2
3
  import type {
3
4
  Pipeline,
@@ -98,6 +99,8 @@ export type Card = {
98
99
  lastMessage?: unknown
99
100
  /** Only on getMany/getById answers: sum of the products (`ammount * value`). */
100
101
  totalValue?: number
102
+ /** Only on `getCards` answers. */
103
+ contactName?: string
101
104
  // relationships
102
105
  contact?: Contact | null
103
106
  owner?: User | null
@@ -158,3 +161,81 @@ export type UpdateCardPayload = {
158
161
  success?: boolean | null
159
162
  finishedAt?: string | null
160
163
  }
164
+
165
+ /** Answer of the product and comment writes — the entity itself is not returned. */
166
+ export type CardOperationResult = { message: string }
167
+
168
+ export type BulkMoveCardsPayload = {
169
+ /** At least one; ids outside the account or not in `originPipelineStageId` are ignored. */
170
+ ids: string[]
171
+ originPipelineStageId: string
172
+ /** Destination pipeline; `pipelineStageId` must be one of its stages. */
173
+ pipelineId: string
174
+ pipelineStageId: string
175
+ /** Must be a status of the destination stage, unless it is the last stage. */
176
+ statusId?: string
177
+ /** Kept only when the destination is the last stage (then `finishedAt` is set). */
178
+ success?: boolean
179
+ reasonId?: string
180
+ }
181
+
182
+ export type BulkMoveCardsResult = { movedCount: number; ignoredCount: number }
183
+
184
+ /**
185
+ * Creates a product of a card, or updates the product of `id`. No validation on the backend:
186
+ * a missing `name`/`ammount`/`value` fails with 500.
187
+ */
188
+ export type SaveCardProductPayload = {
189
+ id?: string
190
+ cardId: string
191
+ name: string
192
+ /** Quantity — integer (the backend column is spelled `ammount`). */
193
+ ammount: number
194
+ value: number
195
+ }
196
+
197
+ /**
198
+ * Creates a comment on `cardId`, or edits the comment of `id`. It needs text or at least one
199
+ * file. `files` are references to files already uploaded (see `FilesApi`) — `.pdf`, `.txt`,
200
+ * `.html` or `.md`, up to 10 files of 10 MB.
201
+ */
202
+ export type SaveCardCommentPayload =
203
+ | { id?: undefined; cardId: string; comment?: string; files?: { id: string }[] }
204
+ | {
205
+ id: string
206
+ cardId?: string
207
+ comment?: string
208
+ files?: { id: string }[]
209
+ removedFileIds?: string[]
210
+ }
211
+
212
+ export type CardOwner = {
213
+ id: string
214
+ /** Suffixed with " - Sem permissão" (in the caller's language) when `disabled`. */
215
+ name: string
216
+ /** `true` for users without `pipelines.opportunity.update`. */
217
+ disabled: boolean
218
+ }
219
+
220
+ /**
221
+ * Body of `POST /cards/getCards`: Sequelize options sent as-is. The route does **not** scope the
222
+ * query by account — without `where.accountId` it answers cards of other accounts. `order`
223
+ * accepts card fields and `contact.name`.
224
+ */
225
+ export type GetCardsPayload = Omit<
226
+ ListQuery<Card>,
227
+ 'page' | 'perPage' | 'paginate' | 'where' | 'order' | 'attributes'
228
+ > & {
229
+ where: WhereClause<Card> & { accountId: string }
230
+ /**
231
+ * The card columns to answer. Required in practice: the backend selects only these (plus the
232
+ * computed `contactName` and `totalValue`), so without them each card has no other field.
233
+ */
234
+ attributes: (keyof Card & string)[]
235
+ order?: [(keyof Card & string) | 'contact.name', 'ASC' | 'DESC'][]
236
+ }
237
+
238
+ export type CardContactTotals = {
239
+ /** Open cards of the contact (not archived, not finished, in a pipeline that exists). */
240
+ totalOpen: number
241
+ }
@@ -1,14 +1,32 @@
1
1
  import type { Department } from '../departments/types'
2
2
  import type { KanbanColumn } from '../kanbanColumns/types'
3
3
 
4
+ export type KanbanBoardVisibleTickets = 'mine' | 'all' | 'mine-and-not-assigned'
5
+
6
+ export type KanbanBoardClosedMaxTimeUnit = 'minutes' | 'hours' | 'days' | 'weeks' | 'months'
7
+
8
+ /** Card fields shown on the board. */
9
+ export type KanbanBoardSettings = {
10
+ lastMessage: boolean
11
+ departmentAndUser: boolean
12
+ tags: boolean
13
+ createdAtDate: boolean
14
+ newInteractions: boolean
15
+ timeoutAlert: boolean
16
+ }
17
+
4
18
  export type KanbanBoard = {
5
19
  id: string
6
20
  name: string
7
- timeoutAlertEnabled: boolean
8
21
  accountId: string
22
+ visibleTickets: KanbanBoardVisibleTickets | null
23
+ closedMaxTime: number | null
24
+ closedMaxTimeUnit: KanbanBoardClosedMaxTimeUnit | null
25
+ /** `'initializing'` while the cards of the open tickets are being created. */
26
+ status: 'initializing' | 'ready'
27
+ settings: KanbanBoardSettings
9
28
  createdAt: string
10
29
  updatedAt: string
11
- deletedAt: string | null
12
30
  // relationships
13
31
  departments?: Department[]
14
32
  columns?: KanbanColumn[]
@@ -18,12 +36,26 @@ export type KanbanBoardRelationships = 'departments' | 'columns'
18
36
 
19
37
  export type CreateKanbanBoardPayload = {
20
38
  name: string
21
- timeoutAlertEnabled?: boolean
22
39
  /** Department IDs to link to the board. Required by the backend on creation. */
23
40
  departmentIds: string[]
41
+ visibleTickets?: KanbanBoardVisibleTickets
42
+ /** Greater than 0. */
43
+ closedMaxTime?: number
44
+ closedMaxTimeUnit?: KanbanBoardClosedMaxTimeUnit
45
+ settings?: Partial<KanbanBoardSettings>
24
46
  }
25
47
 
26
48
  export type UpdateKanbanBoardPayload = {
27
49
  name?: string
28
- timeoutAlertEnabled?: boolean
50
+ /**
51
+ * Replaces the departments of the board (at least one): removing a department deletes the
52
+ * cards of its tickets, adding one creates cards for its open tickets.
53
+ */
54
+ departmentIds?: string[]
55
+ visibleTickets?: KanbanBoardVisibleTickets
56
+ /** Greater than 0. */
57
+ closedMaxTime?: number
58
+ closedMaxTimeUnit?: KanbanBoardClosedMaxTimeUnit
59
+ /** Merged into the current settings — send only the keys to change. */
60
+ settings?: Partial<KanbanBoardSettings>
29
61
  }
@@ -0,0 +1,46 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { ApiClient } from '../../core/ApiClient'
3
+ import { KanbanCardsApi } from './KanbanCardsApi'
4
+
5
+ // ── Mock client factory ──────────────────────────────────────────────
6
+
7
+ function createMockClient(): ApiClient {
8
+ return {
9
+ setAccessToken: vi.fn().mockReturnThis(),
10
+ request: vi.fn(),
11
+ get: vi.fn(),
12
+ post: vi.fn(),
13
+ put: vi.fn(),
14
+ patch: vi.fn(),
15
+ delete: vi.fn(),
16
+ }
17
+ }
18
+
19
+ const headers = { 'X-Custom': 'value' }
20
+
21
+ // ── Tests ────────────────────────────────────────────────────────────
22
+
23
+ describe('KanbanCardsApi', () => {
24
+ let client: ReturnType<typeof createMockClient>
25
+ let api: KanbanCardsApi
26
+
27
+ beforeEach(() => {
28
+ client = createMockClient()
29
+ api = new KanbanCardsApi(client)
30
+ })
31
+
32
+ describe('reorder', () => {
33
+ it('calls client.patch with /kanban-cards/:id/reorder and body', async () => {
34
+ vi.mocked(client.patch).mockResolvedValue({ id: 'kc1', position: 'N' })
35
+ const body = { columnId: 'col2', moveTo: 'end' as const }
36
+ const result = await api.reorder('kc1', body, headers)
37
+ expect(client.patch).toHaveBeenCalledWith('/kanban-cards/kc1/reorder', body, headers)
38
+ expect(result).toEqual({ id: 'kc1', position: 'N' })
39
+ })
40
+
41
+ it('propagates errors', async () => {
42
+ vi.mocked(client.patch).mockRejectedValue(new Error('Invalid reorder operation.'))
43
+ await expect(api.reorder('kc1', {})).rejects.toThrow('Invalid reorder operation.')
44
+ })
45
+ })
46
+ })
@@ -1,6 +1,11 @@
1
1
  import type { ApiClient } from '../../core/ApiClient'
2
2
  import { BaseCrudApi } from '../../core/BaseCrudApi'
3
- import type { KanbanCard, CreateKanbanCardPayload, UpdateKanbanCardPayload } from './types'
3
+ import type {
4
+ KanbanCard,
5
+ CreateKanbanCardPayload,
6
+ ReorderKanbanCardPayload,
7
+ UpdateKanbanCardPayload,
8
+ } from './types'
4
9
 
5
10
  export class KanbanCardsApi extends BaseCrudApi<
6
11
  KanbanCard,
@@ -10,4 +15,21 @@ export class KanbanCardsApi extends BaseCrudApi<
10
15
  constructor(client: ApiClient) {
11
16
  super(client, '/kanban-cards')
12
17
  }
18
+
19
+ /**
20
+ * Moves a card within its column or to another column (see {@link ReorderKanbanCardPayload}).
21
+ * Resolves to the card with its new `position` and `columnId`.
22
+ * @permissions kanban.cards.move
23
+ */
24
+ reorder(
25
+ id: string,
26
+ body: ReorderKanbanCardPayload,
27
+ headers?: Record<string, string>,
28
+ ): Promise<KanbanCard> {
29
+ return this.client.patch<KanbanCard, ReorderKanbanCardPayload>(
30
+ `${this.basePath}/${id}/reorder`,
31
+ body,
32
+ headers,
33
+ )
34
+ }
13
35
  }
@@ -3,7 +3,6 @@ import type { Ticket } from '../tickets/types'
3
3
 
4
4
  export type KanbanCard = {
5
5
  id: string
6
- ticketSubject: string | null
7
6
  position: string
8
7
  columnId: string
9
8
  ticketId: string
@@ -12,10 +11,9 @@ export type KanbanCard = {
12
11
  lastColumnChangeAt: string | null
13
12
  createdAt: string
14
13
  updatedAt: string
15
- deletedAt: string | null
16
14
  // relationships
17
- column?: KanbanColumn
18
- ticket?: Ticket
15
+ column?: KanbanColumn | null
16
+ ticket?: Ticket | null
19
17
  }
20
18
 
21
19
  export type KanbanCardRelationships = 'column' | 'ticket'
@@ -29,5 +27,20 @@ export type CreateKanbanCardPayload = {
29
27
  export type UpdateKanbanCardPayload = {
30
28
  columnId?: string
31
29
  position?: string
32
- ticketSubject?: string | null
30
+ /** The caller must share a department with the ticket (or transfer to all / be admin). */
31
+ ticketId?: string
32
+ }
33
+
34
+ /**
35
+ * Where the card goes. The target column is `columnId`, else the column of the neighbours, else
36
+ * the current one; `moveTo` puts it first/last of the target column. Moving into the `'final'`
37
+ * column closes the ticket, moving out of it reopens the ticket.
38
+ */
39
+ export type ReorderKanbanCardPayload = {
40
+ /** The card that ends up above. Must be in the same column as `afterCardId`. */
41
+ beforeCardId?: string
42
+ /** The card that ends up below. */
43
+ afterCardId?: string
44
+ columnId?: string
45
+ moveTo?: 'start' | 'end'
33
46
  }
@@ -0,0 +1,46 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { ApiClient } from '../../core/ApiClient'
3
+ import { KanbanColumnsApi } from './KanbanColumnsApi'
4
+
5
+ // ── Mock client factory ──────────────────────────────────────────────
6
+
7
+ function createMockClient(): ApiClient {
8
+ return {
9
+ setAccessToken: vi.fn().mockReturnThis(),
10
+ request: vi.fn(),
11
+ get: vi.fn(),
12
+ post: vi.fn(),
13
+ put: vi.fn(),
14
+ patch: vi.fn(),
15
+ delete: vi.fn(),
16
+ }
17
+ }
18
+
19
+ const headers = { 'X-Custom': 'value' }
20
+
21
+ // ── Tests ────────────────────────────────────────────────────────────
22
+
23
+ describe('KanbanColumnsApi', () => {
24
+ let client: ReturnType<typeof createMockClient>
25
+ let api: KanbanColumnsApi
26
+
27
+ beforeEach(() => {
28
+ client = createMockClient()
29
+ api = new KanbanColumnsApi(client)
30
+ })
31
+
32
+ describe('reorder', () => {
33
+ it('calls client.patch with /kanban-columns/:id/reorder and body', async () => {
34
+ vi.mocked(client.patch).mockResolvedValue({ id: 'col1', position: 'N' })
35
+ const body = { beforeColumnId: 'col0', afterColumnId: 'col2' }
36
+ const result = await api.reorder('col1', body, headers)
37
+ expect(client.patch).toHaveBeenCalledWith('/kanban-columns/col1/reorder', body, headers)
38
+ expect(result).toEqual({ id: 'col1', position: 'N' })
39
+ })
40
+
41
+ it('propagates errors', async () => {
42
+ vi.mocked(client.patch).mockRejectedValue(new Error('Invalid reorder operation.'))
43
+ await expect(api.reorder('col1', {})).rejects.toThrow('Invalid reorder operation.')
44
+ })
45
+ })
46
+ })
@@ -1,6 +1,11 @@
1
1
  import type { ApiClient } from '../../core/ApiClient'
2
2
  import { BaseCrudApi } from '../../core/BaseCrudApi'
3
- import type { KanbanColumn, CreateKanbanColumnPayload, UpdateKanbanColumnPayload } from './types'
3
+ import type {
4
+ KanbanColumn,
5
+ CreateKanbanColumnPayload,
6
+ ReorderKanbanColumnPayload,
7
+ UpdateKanbanColumnPayload,
8
+ } from './types'
4
9
 
5
10
  export class KanbanColumnsApi extends BaseCrudApi<
6
11
  KanbanColumn,
@@ -10,4 +15,22 @@ export class KanbanColumnsApi extends BaseCrudApi<
10
15
  constructor(client: ApiClient) {
11
16
  super(client, '/kanban-columns')
12
17
  }
18
+
19
+ /**
20
+ * Moves a column between two neighbours of the same board. Both the column and a neighbour
21
+ * must be draggable; swapped neighbours (`before` after `after`) fail with 500. Resolves to the
22
+ * column with its new `position`.
23
+ * @permissions kanban.columns.update
24
+ */
25
+ reorder(
26
+ id: string,
27
+ body: ReorderKanbanColumnPayload,
28
+ headers?: Record<string, string>,
29
+ ): Promise<KanbanColumn> {
30
+ return this.client.patch<KanbanColumn, ReorderKanbanColumnPayload>(
31
+ `${this.basePath}/${id}/reorder`,
32
+ body,
33
+ headers,
34
+ )
35
+ }
13
36
  }
@@ -14,18 +14,22 @@ export type KanbanColumn = {
14
14
  accountId: string
15
15
  createdAt: string
16
16
  updatedAt: string
17
- deletedAt: string | null
18
17
  // relationships
19
- kanbanBoard?: KanbanBoard
18
+ board?: KanbanBoard | null
20
19
  cards?: KanbanCard[]
21
20
  }
22
21
 
23
- export type KanbanColumnRelationships = 'kanbanBoard' | 'cards'
22
+ export type KanbanColumnRelationships = 'cards'
24
23
 
25
24
  export type CreateKanbanColumnPayload = {
26
25
  name: string
27
26
  kanbanBoardId: string
27
+ /** A board has one `'initial'` and one `'final'` column (a second one fails). */
28
28
  role?: KanbanColumnRole
29
+ /**
30
+ * @deprecated Ignored by the backend: the position is computed (`'initial'` first, `'final'`
31
+ * last, other columns just before the final one). Use `reorder` to move a column.
32
+ */
29
33
  position?: string
30
34
  ticketWaitTimeMinutes?: number
31
35
  isDraggable?: boolean
@@ -38,3 +42,11 @@ export type UpdateKanbanColumnPayload = {
38
42
  ticketWaitTimeMinutes?: number | null
39
43
  isDraggable?: boolean
40
44
  }
45
+
46
+ /** Neighbours of the column after the move (omit both to move it to the middle, `'M'`). */
47
+ export type ReorderKanbanColumnPayload = {
48
+ /** The column that ends up on the left. */
49
+ beforeColumnId?: string
50
+ /** The column that ends up on the right. */
51
+ afterColumnId?: string
52
+ }
@@ -34,15 +34,15 @@ const user: User = {
34
34
  email: 'alice@example.com',
35
35
  phoneNumber: null,
36
36
  branch: null,
37
- isSuperAdmin: false,
38
37
  isClientUser: false,
39
- active: true,
40
38
  language: 'en',
41
39
  isFirstLogin: false,
42
40
  status: 'online',
43
41
  clientsStatus: null,
44
- offlineAt: null,
45
42
  isActiveInternalChat: true,
43
+ internalChatToken: null,
44
+ noteReference: false,
45
+ data: {},
46
46
  passwordExpiresAt: null,
47
47
  otpAuthActive: false,
48
48
  preferences: {},