@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.
- package/dist/apis/cards/CardsApi.cjs +70 -0
- package/dist/apis/cards/CardsApi.cjs.map +1 -1
- package/dist/apis/cards/CardsApi.d.ts +53 -1
- package/dist/apis/cards/CardsApi.d.ts.map +1 -1
- package/dist/apis/cards/CardsApi.mjs +70 -0
- package/dist/apis/cards/CardsApi.mjs.map +1 -1
- package/dist/apis/cards/types.d.ts +84 -0
- package/dist/apis/cards/types.d.ts.map +1 -1
- package/dist/apis/kanbanBoards/types.d.ts +33 -4
- package/dist/apis/kanbanBoards/types.d.ts.map +1 -1
- package/dist/apis/kanbanCards/KanbanCardsApi.cjs +8 -0
- package/dist/apis/kanbanCards/KanbanCardsApi.cjs.map +1 -1
- package/dist/apis/kanbanCards/KanbanCardsApi.d.ts +7 -1
- package/dist/apis/kanbanCards/KanbanCardsApi.d.ts.map +1 -1
- package/dist/apis/kanbanCards/KanbanCardsApi.mjs +8 -0
- package/dist/apis/kanbanCards/KanbanCardsApi.mjs.map +1 -1
- package/dist/apis/kanbanCards/types.d.ts +17 -5
- package/dist/apis/kanbanCards/types.d.ts.map +1 -1
- package/dist/apis/kanbanColumns/KanbanColumnsApi.cjs +9 -0
- package/dist/apis/kanbanColumns/KanbanColumnsApi.cjs.map +1 -1
- package/dist/apis/kanbanColumns/KanbanColumnsApi.d.ts +8 -1
- package/dist/apis/kanbanColumns/KanbanColumnsApi.d.ts.map +1 -1
- package/dist/apis/kanbanColumns/KanbanColumnsApi.mjs +9 -0
- package/dist/apis/kanbanColumns/KanbanColumnsApi.mjs.map +1 -1
- package/dist/apis/kanbanColumns/types.d.ts +14 -3
- package/dist/apis/kanbanColumns/types.d.ts.map +1 -1
- package/dist/apis/messages/MessagesApi.cjs +4 -1
- package/dist/apis/messages/MessagesApi.cjs.map +1 -1
- package/dist/apis/messages/MessagesApi.d.ts +5 -2
- package/dist/apis/messages/MessagesApi.d.ts.map +1 -1
- package/dist/apis/messages/MessagesApi.mjs +4 -1
- package/dist/apis/messages/MessagesApi.mjs.map +1 -1
- package/dist/apis/messages/types.d.ts +13 -0
- package/dist/apis/messages/types.d.ts.map +1 -1
- package/dist/apis/pipeline/PipelineApi.cjs +133 -0
- package/dist/apis/pipeline/PipelineApi.cjs.map +1 -1
- package/dist/apis/pipeline/PipelineApi.d.ts +102 -1
- package/dist/apis/pipeline/PipelineApi.d.ts.map +1 -1
- package/dist/apis/pipeline/PipelineApi.mjs +133 -0
- package/dist/apis/pipeline/PipelineApi.mjs.map +1 -1
- package/dist/apis/pipeline/types.d.ts +164 -0
- package/dist/apis/pipeline/types.d.ts.map +1 -1
- package/dist/apis/tags/TagsApi.cjs +3 -2
- package/dist/apis/tags/TagsApi.cjs.map +1 -1
- package/dist/apis/tags/TagsApi.d.ts +6 -5
- package/dist/apis/tags/TagsApi.d.ts.map +1 -1
- package/dist/apis/tags/TagsApi.mjs +3 -2
- package/dist/apis/tags/TagsApi.mjs.map +1 -1
- package/dist/apis/tags/types.d.ts +5 -0
- package/dist/apis/tags/types.d.ts.map +1 -1
- package/dist/apis/users/types.d.ts +21 -3
- package/dist/apis/users/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/apis/cards/CardsApi.test.ts +128 -0
- package/src/apis/cards/CardsApi.ts +124 -1
- package/src/apis/cards/types.ts +81 -0
- package/src/apis/kanbanBoards/types.ts +36 -4
- package/src/apis/kanbanCards/KanbanCardsApi.test.ts +46 -0
- package/src/apis/kanbanCards/KanbanCardsApi.ts +23 -1
- package/src/apis/kanbanCards/types.ts +18 -5
- package/src/apis/kanbanColumns/KanbanColumnsApi.test.ts +46 -0
- package/src/apis/kanbanColumns/KanbanColumnsApi.ts +24 -1
- package/src/apis/kanbanColumns/types.ts +15 -3
- package/src/apis/me/MeApi.test.ts +3 -3
- package/src/apis/messages/MessagesApi.test.ts +11 -0
- package/src/apis/messages/MessagesApi.ts +6 -3
- package/src/apis/messages/types.ts +13 -0
- package/src/apis/payloads.test-d.ts +30 -0
- package/src/apis/pipeline/PipelineApi.test.ts +193 -0
- package/src/apis/pipeline/PipelineApi.ts +274 -1
- package/src/apis/pipeline/types.ts +162 -0
- package/src/apis/tags/TagsApi.test.ts +5 -0
- package/src/apis/tags/TagsApi.ts +8 -6
- package/src/apis/tags/types.ts +6 -0
- package/src/apis/users/UsersApi.test.ts +3 -3
- 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 {
|
|
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
|
}
|
package/src/apis/cards/types.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
18
|
+
board?: KanbanBoard | null
|
|
20
19
|
cards?: KanbanCard[]
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
export type KanbanColumnRelationships = '
|
|
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: {},
|