@open-mercato/core 0.6.6-develop.6314.1.c7b8291aa2 → 0.6.6-develop.6317.1.b3be10ab84
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/ai-tools/_shared.js +158 -0
- package/dist/modules/customers/ai-tools/_shared.js.map +7 -0
- package/dist/modules/customers/ai-tools/companies-pack.js +15 -121
- package/dist/modules/customers/ai-tools/companies-pack.js.map +2 -2
- package/dist/modules/customers/ai-tools/people-pack.js +9 -107
- package/dist/modules/customers/ai-tools/people-pack.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarScreen.js +1 -0
- package/dist/modules/customers/components/calendar/CalendarScreen.js.map +2 -2
- package/dist/modules/customers/components/calendar/EventPeekPopover.js +12 -1
- package/dist/modules/customers/components/calendar/EventPeekPopover.js.map +2 -2
- package/dist/modules/customers/components/calendar/TimeGrid.js +3 -0
- package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/types.js.map +1 -1
- package/package.json +7 -7
- package/src/modules/customers/ai-tools/_shared.ts +270 -0
- package/src/modules/customers/ai-tools/companies-pack.ts +17 -157
- package/src/modules/customers/ai-tools/people-pack.ts +11 -133
- package/src/modules/customers/components/calendar/CalendarScreen.tsx +1 -0
- package/src/modules/customers/components/calendar/EventPeekPopover.tsx +29 -11
- package/src/modules/customers/components/calendar/TimeGrid.tsx +3 -0
- package/src/modules/customers/components/calendar/types.ts +1 -0
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6317.1.b3be10ab84",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -246,16 +246,16 @@
|
|
|
246
246
|
"zod": "^4.4.3"
|
|
247
247
|
},
|
|
248
248
|
"peerDependencies": {
|
|
249
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
250
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
251
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
249
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6317.1.b3be10ab84",
|
|
250
|
+
"@open-mercato/shared": "0.6.6-develop.6317.1.b3be10ab84",
|
|
251
|
+
"@open-mercato/ui": "0.6.6-develop.6317.1.b3be10ab84",
|
|
252
252
|
"react": "^19.0.0",
|
|
253
253
|
"react-dom": "^19.0.0"
|
|
254
254
|
},
|
|
255
255
|
"devDependencies": {
|
|
256
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
256
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6317.1.b3be10ab84",
|
|
257
|
+
"@open-mercato/shared": "0.6.6-develop.6317.1.b3be10ab84",
|
|
258
|
+
"@open-mercato/ui": "0.6.6-develop.6317.1.b3be10ab84",
|
|
259
259
|
"@testing-library/dom": "^10.4.1",
|
|
260
260
|
"@testing-library/jest-dom": "^6.9.1",
|
|
261
261
|
"@testing-library/react": "^16.3.1",
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for customers AI tool packs.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the catalog precedent (`packages/core/src/modules/catalog/ai-tools/_shared.ts`):
|
|
5
|
+
* the company and people packs previously carried byte-for-byte-identical copies
|
|
6
|
+
* of the date-to-ISO helper, the `resolveEm` / `buildScope` accessors, the
|
|
7
|
+
* list-row → summary mapper, and the related-records builder
|
|
8
|
+
* (addresses / activities / notes / tasks / interactions / tags / deals, plus a
|
|
9
|
+
* companies-only `people` mapper). Centralizing them here gives both packs one
|
|
10
|
+
* source of truth so a change to the related-records output shape no longer has
|
|
11
|
+
* to be applied — and kept in sync — in two places.
|
|
12
|
+
*
|
|
13
|
+
* This is a pure internal refactor: tool names, input schemas, `requiredFeatures`,
|
|
14
|
+
* and emitted output shapes stay identical.
|
|
15
|
+
*/
|
|
16
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
17
|
+
import type { AiToolExecutionContext } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'
|
|
18
|
+
import type { CustomersToolContext } from './types'
|
|
19
|
+
|
|
20
|
+
export function toIso(value: unknown): string | null {
|
|
21
|
+
if (!value) return null
|
|
22
|
+
const dt = value instanceof Date ? value : new Date(String(value))
|
|
23
|
+
if (Number.isNaN(dt.getTime())) return null
|
|
24
|
+
return dt.toISOString()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveEm(ctx: CustomersToolContext | AiToolExecutionContext): EntityManager {
|
|
28
|
+
return ctx.container.resolve<EntityManager>('em')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildScope(ctx: CustomersToolContext | AiToolExecutionContext, tenantId: string) {
|
|
32
|
+
return {
|
|
33
|
+
tenantId,
|
|
34
|
+
organizationId: ctx.organizationId,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* -------------------------------------------------------------------------- */
|
|
39
|
+
/* List-row summary mapper */
|
|
40
|
+
/* -------------------------------------------------------------------------- */
|
|
41
|
+
|
|
42
|
+
export type CustomerListApiItemBase = {
|
|
43
|
+
id?: string
|
|
44
|
+
display_name?: string | null
|
|
45
|
+
displayName?: string | null
|
|
46
|
+
primary_email?: string | null
|
|
47
|
+
primaryEmail?: string | null
|
|
48
|
+
primary_phone?: string | null
|
|
49
|
+
primaryPhone?: string | null
|
|
50
|
+
status?: string | null
|
|
51
|
+
lifecycle_stage?: string | null
|
|
52
|
+
lifecycleStage?: string | null
|
|
53
|
+
source?: string | null
|
|
54
|
+
owner_user_id?: string | null
|
|
55
|
+
ownerUserId?: string | null
|
|
56
|
+
organization_id?: string | null
|
|
57
|
+
organizationId?: string | null
|
|
58
|
+
tenant_id?: string | null
|
|
59
|
+
tenantId?: string | null
|
|
60
|
+
created_at?: string | null
|
|
61
|
+
createdAt?: string | null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type CustomerListSummary = {
|
|
65
|
+
id: string | undefined
|
|
66
|
+
displayName: string | null
|
|
67
|
+
primaryEmail: string | null
|
|
68
|
+
primaryPhone: string | null
|
|
69
|
+
status: string | null
|
|
70
|
+
lifecycleStage: string | null
|
|
71
|
+
source: string | null
|
|
72
|
+
ownerUserId: string | null
|
|
73
|
+
organizationId: string | null
|
|
74
|
+
tenantId: string | null
|
|
75
|
+
createdAt: string | null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function toCustomerListSummary(row: CustomerListApiItemBase): CustomerListSummary {
|
|
79
|
+
const createdAtRaw = row.created_at ?? row.createdAt ?? null
|
|
80
|
+
const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null
|
|
81
|
+
return {
|
|
82
|
+
id: row.id,
|
|
83
|
+
displayName: row.display_name ?? row.displayName ?? null,
|
|
84
|
+
primaryEmail: row.primary_email ?? row.primaryEmail ?? null,
|
|
85
|
+
primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,
|
|
86
|
+
status: row.status ?? null,
|
|
87
|
+
lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,
|
|
88
|
+
source: row.source ?? null,
|
|
89
|
+
ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,
|
|
90
|
+
organizationId: row.organization_id ?? row.organizationId ?? null,
|
|
91
|
+
tenantId: row.tenant_id ?? row.tenantId ?? null,
|
|
92
|
+
createdAt,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* -------------------------------------------------------------------------- */
|
|
97
|
+
/* Related-records mappers */
|
|
98
|
+
/* -------------------------------------------------------------------------- */
|
|
99
|
+
|
|
100
|
+
function asRows(value: unknown): Array<Record<string, unknown>> {
|
|
101
|
+
return Array.isArray(value) ? (value as Array<Record<string, unknown>>) : []
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function mapAddresses(rows: Array<Record<string, unknown>>) {
|
|
105
|
+
return rows.map((address) => ({
|
|
106
|
+
id: address.id,
|
|
107
|
+
name: address.name ?? null,
|
|
108
|
+
purpose: address.purpose ?? null,
|
|
109
|
+
addressLine1: address.addressLine1 ?? null,
|
|
110
|
+
addressLine2: address.addressLine2 ?? null,
|
|
111
|
+
city: address.city ?? null,
|
|
112
|
+
region: address.region ?? null,
|
|
113
|
+
postalCode: address.postalCode ?? null,
|
|
114
|
+
country: address.country ?? null,
|
|
115
|
+
isPrimary: !!address.isPrimary,
|
|
116
|
+
}))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function mapActivities(rows: Array<Record<string, unknown>>) {
|
|
120
|
+
return rows.map((activity) => ({
|
|
121
|
+
id: activity.id,
|
|
122
|
+
activityType: activity.activityType,
|
|
123
|
+
subject: activity.subject ?? null,
|
|
124
|
+
body: activity.body ?? null,
|
|
125
|
+
occurredAt: toIso(activity.occurredAt),
|
|
126
|
+
createdAt: toIso(activity.createdAt),
|
|
127
|
+
}))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function mapNotes(rows: Array<Record<string, unknown>>) {
|
|
131
|
+
return rows.map((comment) => ({
|
|
132
|
+
id: comment.id,
|
|
133
|
+
body: comment.body,
|
|
134
|
+
authorUserId: comment.authorUserId ?? null,
|
|
135
|
+
createdAt: toIso(comment.createdAt),
|
|
136
|
+
}))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function mapTasks(rows: Array<Record<string, unknown>>) {
|
|
140
|
+
return rows.map((task) => ({
|
|
141
|
+
id: task.id,
|
|
142
|
+
todoId: task.todoId ?? task.id,
|
|
143
|
+
todoSource: task.todoSource ?? null,
|
|
144
|
+
createdAt: toIso(task.createdAt),
|
|
145
|
+
}))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function mapInteractions(rows: Array<Record<string, unknown>>) {
|
|
149
|
+
return rows.map((interaction) => ({
|
|
150
|
+
id: interaction.id,
|
|
151
|
+
interactionType: interaction.interactionType,
|
|
152
|
+
title: interaction.title ?? null,
|
|
153
|
+
status: interaction.status,
|
|
154
|
+
scheduledAt: toIso(interaction.scheduledAt),
|
|
155
|
+
occurredAt: toIso(interaction.occurredAt),
|
|
156
|
+
}))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export type RelatedTag = { id: string; slug: string; label: string; color: string | null }
|
|
160
|
+
|
|
161
|
+
export function mapTags(rows: Array<Record<string, unknown>>): RelatedTag[] {
|
|
162
|
+
return rows
|
|
163
|
+
.map((tag) => {
|
|
164
|
+
if (!tag || typeof tag !== 'object') return null
|
|
165
|
+
const id = typeof tag.id === 'string' ? tag.id : null
|
|
166
|
+
const label = typeof tag.label === 'string' ? tag.label : null
|
|
167
|
+
if (!id || !label) return null
|
|
168
|
+
const slug = typeof tag.slug === 'string' ? tag.slug : label
|
|
169
|
+
const color = typeof tag.color === 'string' ? tag.color : null
|
|
170
|
+
return { id, slug, label, color }
|
|
171
|
+
})
|
|
172
|
+
.filter((entry): entry is RelatedTag => entry !== null)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export type RelatedDeal = {
|
|
176
|
+
id: string
|
|
177
|
+
title: string
|
|
178
|
+
status: string | null
|
|
179
|
+
pipelineStageId: string | null
|
|
180
|
+
valueAmount: string | null
|
|
181
|
+
valueCurrency: string | null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function mapDeals(rows: Array<Record<string, unknown>>): RelatedDeal[] {
|
|
185
|
+
return rows
|
|
186
|
+
.map((deal) => {
|
|
187
|
+
if (!deal || typeof deal !== 'object') return null
|
|
188
|
+
const id = typeof deal.id === 'string' ? deal.id : null
|
|
189
|
+
if (!id) return null
|
|
190
|
+
return {
|
|
191
|
+
id,
|
|
192
|
+
title: typeof deal.title === 'string' ? deal.title : '',
|
|
193
|
+
status: typeof deal.status === 'string' ? deal.status : null,
|
|
194
|
+
pipelineStageId: typeof deal.pipelineStageId === 'string' ? deal.pipelineStageId : null,
|
|
195
|
+
valueAmount:
|
|
196
|
+
typeof deal.valueAmount === 'string'
|
|
197
|
+
? deal.valueAmount
|
|
198
|
+
: deal.valueAmount === null || deal.valueAmount === undefined
|
|
199
|
+
? null
|
|
200
|
+
: String(deal.valueAmount),
|
|
201
|
+
valueCurrency: typeof deal.valueCurrency === 'string' ? deal.valueCurrency : null,
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
.filter((value): value is RelatedDeal => value !== null)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export type RelatedPerson = {
|
|
208
|
+
id: string
|
|
209
|
+
displayName: string
|
|
210
|
+
primaryEmail: string | null
|
|
211
|
+
primaryPhone: string | null
|
|
212
|
+
jobTitle: string | null
|
|
213
|
+
department: string | null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function mapPeople(rows: Array<Record<string, unknown>>): RelatedPerson[] {
|
|
217
|
+
return rows
|
|
218
|
+
.map((person) => {
|
|
219
|
+
if (!person || typeof person !== 'object') return null
|
|
220
|
+
const id = typeof person.id === 'string' ? person.id : null
|
|
221
|
+
const displayName = typeof person.displayName === 'string' ? person.displayName : null
|
|
222
|
+
if (!id || !displayName) return null
|
|
223
|
+
return {
|
|
224
|
+
id,
|
|
225
|
+
displayName,
|
|
226
|
+
primaryEmail: typeof person.primaryEmail === 'string' ? person.primaryEmail : null,
|
|
227
|
+
primaryPhone: typeof person.primaryPhone === 'string' ? person.primaryPhone : null,
|
|
228
|
+
jobTitle: typeof person.jobTitle === 'string' ? person.jobTitle : null,
|
|
229
|
+
department: typeof person.department === 'string' ? person.department : null,
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
.filter((value): value is RelatedPerson => value !== null)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export type CustomerRelatedRecords = {
|
|
236
|
+
addresses: ReturnType<typeof mapAddresses>
|
|
237
|
+
activities: ReturnType<typeof mapActivities>
|
|
238
|
+
notes: ReturnType<typeof mapNotes>
|
|
239
|
+
tasks: ReturnType<typeof mapTasks>
|
|
240
|
+
interactions: ReturnType<typeof mapInteractions>
|
|
241
|
+
tags: RelatedTag[]
|
|
242
|
+
deals: RelatedDeal[]
|
|
243
|
+
people?: RelatedPerson[]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Builds the related-records block from a customers detail API payload
|
|
248
|
+
* (`addresses`, `activities`, `comments`, `todos`, `interactions`, `tags`,
|
|
249
|
+
* `deals`, and — for companies — `people`). The key order matches the
|
|
250
|
+
* pre-refactor literals so the emitted shape is unchanged. Pass
|
|
251
|
+
* `includePeople: true` to add the companies-only `people` collection.
|
|
252
|
+
*/
|
|
253
|
+
export function buildRelatedRecords(
|
|
254
|
+
data: Record<string, unknown>,
|
|
255
|
+
options: { includePeople?: boolean } = {},
|
|
256
|
+
): CustomerRelatedRecords {
|
|
257
|
+
const related: CustomerRelatedRecords = {
|
|
258
|
+
addresses: mapAddresses(asRows(data.addresses)),
|
|
259
|
+
activities: mapActivities(asRows(data.activities)),
|
|
260
|
+
notes: mapNotes(asRows(data.comments)),
|
|
261
|
+
tasks: mapTasks(asRows(data.todos)),
|
|
262
|
+
interactions: mapInteractions(asRows(data.interactions)),
|
|
263
|
+
tags: mapTags(asRows(data.tags)),
|
|
264
|
+
deals: mapDeals(asRows(data.deals)),
|
|
265
|
+
}
|
|
266
|
+
if (options.includePeople) {
|
|
267
|
+
related.people = mapPeople(asRows(data.people))
|
|
268
|
+
}
|
|
269
|
+
return related
|
|
270
|
+
}
|
|
@@ -19,6 +19,12 @@ import {
|
|
|
19
19
|
type AiToolExecutionContext,
|
|
20
20
|
} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'
|
|
21
21
|
import { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'
|
|
22
|
+
import {
|
|
23
|
+
buildRelatedRecords,
|
|
24
|
+
toCustomerListSummary,
|
|
25
|
+
toIso,
|
|
26
|
+
type CustomerRelatedRecords,
|
|
27
|
+
} from './_shared'
|
|
22
28
|
|
|
23
29
|
const listCompaniesInput = z
|
|
24
30
|
.object({
|
|
@@ -108,27 +114,13 @@ const listCompaniesTool = defineApiBackedAiTool<
|
|
|
108
114
|
const data = (response.data ?? {}) as ListCompaniesApiResponse
|
|
109
115
|
const rawItems: ListCompaniesApiItem[] = Array.isArray(data.items) ? data.items : []
|
|
110
116
|
return {
|
|
111
|
-
items: rawItems.map((row) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,
|
|
119
|
-
status: row.status ?? null,
|
|
120
|
-
lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,
|
|
121
|
-
source: row.source ?? null,
|
|
122
|
-
ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,
|
|
123
|
-
organizationId: row.organization_id ?? row.organizationId ?? null,
|
|
124
|
-
tenantId: row.tenant_id ?? row.tenantId ?? null,
|
|
125
|
-
domain: row.domain ?? null,
|
|
126
|
-
websiteUrl: row.website_url ?? row.websiteUrl ?? null,
|
|
127
|
-
industry: row.industry ?? null,
|
|
128
|
-
sizeBucket: row.size_bucket ?? row.sizeBucket ?? null,
|
|
129
|
-
createdAt,
|
|
130
|
-
}
|
|
131
|
-
}),
|
|
117
|
+
items: rawItems.map((row) => ({
|
|
118
|
+
...toCustomerListSummary(row),
|
|
119
|
+
domain: row.domain ?? null,
|
|
120
|
+
websiteUrl: row.website_url ?? row.websiteUrl ?? null,
|
|
121
|
+
industry: row.industry ?? null,
|
|
122
|
+
sizeBucket: row.size_bucket ?? row.sizeBucket ?? null,
|
|
123
|
+
})),
|
|
132
124
|
total: typeof data.total === 'number' ? data.total : 0,
|
|
133
125
|
limit,
|
|
134
126
|
offset,
|
|
@@ -146,13 +138,6 @@ const getCompanyInput = z.object({
|
|
|
146
138
|
|
|
147
139
|
type GetCompanyInput = z.infer<typeof getCompanyInput>
|
|
148
140
|
|
|
149
|
-
function toIsoCompany(value: unknown): string | null {
|
|
150
|
-
if (!value) return null
|
|
151
|
-
const dt = value instanceof Date ? value : new Date(String(value))
|
|
152
|
-
if (Number.isNaN(dt.getTime())) return null
|
|
153
|
-
return dt.toISOString()
|
|
154
|
-
}
|
|
155
|
-
|
|
156
141
|
const getCompanyTool: CustomersAiToolDefinition = {
|
|
157
142
|
name: 'customers.get_company',
|
|
158
143
|
displayName: 'Get company',
|
|
@@ -193,134 +178,9 @@ const getCompanyTool: CustomersAiToolDefinition = {
|
|
|
193
178
|
const profileRow = (data.profile ?? null) as Record<string, unknown> | null
|
|
194
179
|
const customFields = (data.customFields ?? {}) as Record<string, unknown>
|
|
195
180
|
|
|
196
|
-
let related:
|
|
181
|
+
let related: CustomerRelatedRecords | null = null
|
|
197
182
|
if (includeRelated) {
|
|
198
|
-
|
|
199
|
-
const activities = Array.isArray(data.activities) ? (data.activities as Array<Record<string, unknown>>) : []
|
|
200
|
-
const notes = Array.isArray(data.comments) ? (data.comments as Array<Record<string, unknown>>) : []
|
|
201
|
-
const todos = Array.isArray(data.todos) ? (data.todos as Array<Record<string, unknown>>) : []
|
|
202
|
-
const interactions = Array.isArray(data.interactions) ? (data.interactions as Array<Record<string, unknown>>) : []
|
|
203
|
-
const tagsRows = Array.isArray(data.tags) ? (data.tags as Array<Record<string, unknown>>) : []
|
|
204
|
-
const dealsRows = Array.isArray(data.deals) ? (data.deals as Array<Record<string, unknown>>) : []
|
|
205
|
-
const peopleRows = Array.isArray(data.people) ? (data.people as Array<Record<string, unknown>>) : []
|
|
206
|
-
related = {
|
|
207
|
-
addresses: addresses.map((address) => ({
|
|
208
|
-
id: address.id,
|
|
209
|
-
name: address.name ?? null,
|
|
210
|
-
purpose: address.purpose ?? null,
|
|
211
|
-
addressLine1: address.addressLine1 ?? null,
|
|
212
|
-
addressLine2: address.addressLine2 ?? null,
|
|
213
|
-
city: address.city ?? null,
|
|
214
|
-
region: address.region ?? null,
|
|
215
|
-
postalCode: address.postalCode ?? null,
|
|
216
|
-
country: address.country ?? null,
|
|
217
|
-
isPrimary: !!address.isPrimary,
|
|
218
|
-
})),
|
|
219
|
-
activities: activities.map((activity) => ({
|
|
220
|
-
id: activity.id,
|
|
221
|
-
activityType: activity.activityType,
|
|
222
|
-
subject: activity.subject ?? null,
|
|
223
|
-
body: activity.body ?? null,
|
|
224
|
-
occurredAt: toIsoCompany(activity.occurredAt),
|
|
225
|
-
createdAt: toIsoCompany(activity.createdAt),
|
|
226
|
-
})),
|
|
227
|
-
notes: notes.map((comment) => ({
|
|
228
|
-
id: comment.id,
|
|
229
|
-
body: comment.body,
|
|
230
|
-
authorUserId: comment.authorUserId ?? null,
|
|
231
|
-
createdAt: toIsoCompany(comment.createdAt),
|
|
232
|
-
})),
|
|
233
|
-
tasks: todos.map((task) => ({
|
|
234
|
-
id: task.id,
|
|
235
|
-
todoId: task.todoId ?? task.id,
|
|
236
|
-
todoSource: task.todoSource ?? null,
|
|
237
|
-
createdAt: toIsoCompany(task.createdAt),
|
|
238
|
-
})),
|
|
239
|
-
interactions: interactions.map((interaction) => ({
|
|
240
|
-
id: interaction.id,
|
|
241
|
-
interactionType: interaction.interactionType,
|
|
242
|
-
title: interaction.title ?? null,
|
|
243
|
-
status: interaction.status,
|
|
244
|
-
scheduledAt: toIsoCompany(interaction.scheduledAt),
|
|
245
|
-
occurredAt: toIsoCompany(interaction.occurredAt),
|
|
246
|
-
})),
|
|
247
|
-
tags: tagsRows
|
|
248
|
-
.map((tag) => {
|
|
249
|
-
if (!tag || typeof tag !== 'object') return null
|
|
250
|
-
const id = typeof tag.id === 'string' ? tag.id : null
|
|
251
|
-
const label = typeof tag.label === 'string' ? tag.label : null
|
|
252
|
-
if (!id || !label) return null
|
|
253
|
-
const slug = typeof tag.slug === 'string' ? tag.slug : label
|
|
254
|
-
const color = typeof tag.color === 'string' ? tag.color : null
|
|
255
|
-
return { id, slug, label, color }
|
|
256
|
-
})
|
|
257
|
-
.filter(
|
|
258
|
-
(entry): entry is { id: string; slug: string; label: string; color: string | null } =>
|
|
259
|
-
entry !== null,
|
|
260
|
-
),
|
|
261
|
-
deals: dealsRows
|
|
262
|
-
.map((deal) => {
|
|
263
|
-
if (!deal || typeof deal !== 'object') return null
|
|
264
|
-
const id = typeof deal.id === 'string' ? deal.id : null
|
|
265
|
-
if (!id) return null
|
|
266
|
-
return {
|
|
267
|
-
id,
|
|
268
|
-
title: typeof deal.title === 'string' ? deal.title : '',
|
|
269
|
-
status: typeof deal.status === 'string' ? deal.status : null,
|
|
270
|
-
pipelineStageId:
|
|
271
|
-
typeof deal.pipelineStageId === 'string' ? deal.pipelineStageId : null,
|
|
272
|
-
valueAmount:
|
|
273
|
-
typeof deal.valueAmount === 'string'
|
|
274
|
-
? deal.valueAmount
|
|
275
|
-
: deal.valueAmount === null || deal.valueAmount === undefined
|
|
276
|
-
? null
|
|
277
|
-
: String(deal.valueAmount),
|
|
278
|
-
valueCurrency:
|
|
279
|
-
typeof deal.valueCurrency === 'string' ? deal.valueCurrency : null,
|
|
280
|
-
}
|
|
281
|
-
})
|
|
282
|
-
.filter(
|
|
283
|
-
(
|
|
284
|
-
value,
|
|
285
|
-
): value is {
|
|
286
|
-
id: string
|
|
287
|
-
title: string
|
|
288
|
-
status: string | null
|
|
289
|
-
pipelineStageId: string | null
|
|
290
|
-
valueAmount: string | null
|
|
291
|
-
valueCurrency: string | null
|
|
292
|
-
} => value !== null,
|
|
293
|
-
),
|
|
294
|
-
people: peopleRows
|
|
295
|
-
.map((person) => {
|
|
296
|
-
if (!person || typeof person !== 'object') return null
|
|
297
|
-
const id = typeof person.id === 'string' ? person.id : null
|
|
298
|
-
const displayName = typeof person.displayName === 'string' ? person.displayName : null
|
|
299
|
-
if (!id || !displayName) return null
|
|
300
|
-
return {
|
|
301
|
-
id,
|
|
302
|
-
displayName,
|
|
303
|
-
primaryEmail:
|
|
304
|
-
typeof person.primaryEmail === 'string' ? person.primaryEmail : null,
|
|
305
|
-
primaryPhone:
|
|
306
|
-
typeof person.primaryPhone === 'string' ? person.primaryPhone : null,
|
|
307
|
-
jobTitle: typeof person.jobTitle === 'string' ? person.jobTitle : null,
|
|
308
|
-
department: typeof person.department === 'string' ? person.department : null,
|
|
309
|
-
}
|
|
310
|
-
})
|
|
311
|
-
.filter(
|
|
312
|
-
(
|
|
313
|
-
value,
|
|
314
|
-
): value is {
|
|
315
|
-
id: string
|
|
316
|
-
displayName: string
|
|
317
|
-
primaryEmail: string | null
|
|
318
|
-
primaryPhone: string | null
|
|
319
|
-
jobTitle: string | null
|
|
320
|
-
department: string | null
|
|
321
|
-
} => value !== null,
|
|
322
|
-
),
|
|
323
|
-
}
|
|
183
|
+
related = buildRelatedRecords(data, { includePeople: true })
|
|
324
184
|
}
|
|
325
185
|
return {
|
|
326
186
|
found: true as const,
|
|
@@ -336,8 +196,8 @@ const getCompanyTool: CustomersAiToolDefinition = {
|
|
|
336
196
|
ownerUserId: companyRow.ownerUserId ?? null,
|
|
337
197
|
organizationId: companyRow.organizationId ?? null,
|
|
338
198
|
tenantId: companyRow.tenantId ?? null,
|
|
339
|
-
createdAt:
|
|
340
|
-
updatedAt:
|
|
199
|
+
createdAt: toIso(companyRow.createdAt),
|
|
200
|
+
updatedAt: toIso(companyRow.updatedAt),
|
|
341
201
|
},
|
|
342
202
|
profile: profileRow
|
|
343
203
|
? {
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
* documented aggregate detail route). Tool name, schema, requiredFeatures,
|
|
18
18
|
* and output shape are unchanged.
|
|
19
19
|
*/
|
|
20
|
-
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
21
20
|
import { z } from 'zod'
|
|
22
21
|
import { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'
|
|
23
22
|
import {
|
|
@@ -30,20 +29,17 @@ import {
|
|
|
30
29
|
CustomerPersonProfile,
|
|
31
30
|
} from '../data/entities'
|
|
32
31
|
import { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'
|
|
32
|
+
import {
|
|
33
|
+
buildRelatedRecords,
|
|
34
|
+
buildScope,
|
|
35
|
+
resolveEm,
|
|
36
|
+
toCustomerListSummary,
|
|
37
|
+
toIso,
|
|
38
|
+
type CustomerRelatedRecords,
|
|
39
|
+
} from './_shared'
|
|
33
40
|
|
|
34
41
|
const NIL_UUID = '00000000-0000-0000-0000-000000000000'
|
|
35
42
|
|
|
36
|
-
function resolveEm(ctx: CustomersToolContext | AiToolExecutionContext): EntityManager {
|
|
37
|
-
return ctx.container.resolve<EntityManager>('em')
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function buildScope(ctx: CustomersToolContext | AiToolExecutionContext, tenantId: string) {
|
|
41
|
-
return {
|
|
42
|
-
tenantId,
|
|
43
|
-
organizationId: ctx.organizationId,
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
43
|
const listPeopleInput = z
|
|
48
44
|
.object({
|
|
49
45
|
q: z.string().trim().optional().describe('Optional search text matched against display name / email / phone. Omit or leave empty to list all.'),
|
|
@@ -146,23 +142,7 @@ const listPeopleTool = defineApiBackedAiTool<ListPeopleInput, ListPeopleApiRespo
|
|
|
146
142
|
const data = (response.data ?? {}) as ListPeopleApiResponse
|
|
147
143
|
const rawItems: ListPeopleApiItem[] = Array.isArray(data.items) ? data.items : []
|
|
148
144
|
return {
|
|
149
|
-
items: rawItems.map((row) =>
|
|
150
|
-
const createdAtRaw = row.created_at ?? row.createdAt ?? null
|
|
151
|
-
const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null
|
|
152
|
-
return {
|
|
153
|
-
id: row.id,
|
|
154
|
-
displayName: row.display_name ?? row.displayName ?? null,
|
|
155
|
-
primaryEmail: row.primary_email ?? row.primaryEmail ?? null,
|
|
156
|
-
primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,
|
|
157
|
-
status: row.status ?? null,
|
|
158
|
-
lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,
|
|
159
|
-
source: row.source ?? null,
|
|
160
|
-
ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,
|
|
161
|
-
organizationId: row.organization_id ?? row.organizationId ?? null,
|
|
162
|
-
tenantId: row.tenant_id ?? row.tenantId ?? null,
|
|
163
|
-
createdAt,
|
|
164
|
-
}
|
|
165
|
-
}),
|
|
145
|
+
items: rawItems.map((row) => toCustomerListSummary(row)),
|
|
166
146
|
total: typeof data.total === 'number' ? data.total : 0,
|
|
167
147
|
limit,
|
|
168
148
|
offset,
|
|
@@ -182,13 +162,6 @@ type GetPersonInput = z.infer<typeof getPersonInput>
|
|
|
182
162
|
|
|
183
163
|
type ApiPersonDetailRow = Record<string, unknown> | null | undefined
|
|
184
164
|
|
|
185
|
-
function toIso(value: unknown): string | null {
|
|
186
|
-
if (!value) return null
|
|
187
|
-
const dt = value instanceof Date ? value : new Date(String(value))
|
|
188
|
-
if (Number.isNaN(dt.getTime())) return null
|
|
189
|
-
return dt.toISOString()
|
|
190
|
-
}
|
|
191
|
-
|
|
192
165
|
const getPersonTool: CustomersAiToolDefinition = {
|
|
193
166
|
name: 'customers.get_person',
|
|
194
167
|
displayName: 'Get person',
|
|
@@ -226,104 +199,9 @@ const getPersonTool: CustomersAiToolDefinition = {
|
|
|
226
199
|
const profileRow = (data.profile ?? null) as ApiPersonDetailRow
|
|
227
200
|
const customFields = (data.customFields ?? {}) as Record<string, unknown>
|
|
228
201
|
|
|
229
|
-
let related:
|
|
202
|
+
let related: CustomerRelatedRecords | null = null
|
|
230
203
|
if (includeRelated) {
|
|
231
|
-
|
|
232
|
-
const activities = Array.isArray(data.activities) ? (data.activities as Array<Record<string, unknown>>) : []
|
|
233
|
-
const notes = Array.isArray(data.comments) ? (data.comments as Array<Record<string, unknown>>) : []
|
|
234
|
-
const todos = Array.isArray(data.todos) ? (data.todos as Array<Record<string, unknown>>) : []
|
|
235
|
-
const interactions = Array.isArray(data.interactions) ? (data.interactions as Array<Record<string, unknown>>) : []
|
|
236
|
-
const tagsRows = Array.isArray(data.tags) ? (data.tags as Array<Record<string, unknown>>) : []
|
|
237
|
-
const dealsRows = Array.isArray(data.deals) ? (data.deals as Array<Record<string, unknown>>) : []
|
|
238
|
-
related = {
|
|
239
|
-
addresses: addresses.map((address) => ({
|
|
240
|
-
id: address.id,
|
|
241
|
-
name: address.name ?? null,
|
|
242
|
-
purpose: address.purpose ?? null,
|
|
243
|
-
addressLine1: address.addressLine1 ?? null,
|
|
244
|
-
addressLine2: address.addressLine2 ?? null,
|
|
245
|
-
city: address.city ?? null,
|
|
246
|
-
region: address.region ?? null,
|
|
247
|
-
postalCode: address.postalCode ?? null,
|
|
248
|
-
country: address.country ?? null,
|
|
249
|
-
isPrimary: !!address.isPrimary,
|
|
250
|
-
})),
|
|
251
|
-
activities: activities.map((activity) => ({
|
|
252
|
-
id: activity.id,
|
|
253
|
-
activityType: activity.activityType,
|
|
254
|
-
subject: activity.subject ?? null,
|
|
255
|
-
body: activity.body ?? null,
|
|
256
|
-
occurredAt: toIso(activity.occurredAt),
|
|
257
|
-
createdAt: toIso(activity.createdAt),
|
|
258
|
-
})),
|
|
259
|
-
notes: notes.map((comment) => ({
|
|
260
|
-
id: comment.id,
|
|
261
|
-
body: comment.body,
|
|
262
|
-
authorUserId: comment.authorUserId ?? null,
|
|
263
|
-
createdAt: toIso(comment.createdAt),
|
|
264
|
-
})),
|
|
265
|
-
tasks: todos.map((task) => ({
|
|
266
|
-
id: task.id,
|
|
267
|
-
todoId: task.todoId ?? task.id,
|
|
268
|
-
todoSource: task.todoSource ?? null,
|
|
269
|
-
createdAt: toIso(task.createdAt),
|
|
270
|
-
})),
|
|
271
|
-
interactions: interactions.map((interaction) => ({
|
|
272
|
-
id: interaction.id,
|
|
273
|
-
interactionType: interaction.interactionType,
|
|
274
|
-
title: interaction.title ?? null,
|
|
275
|
-
status: interaction.status,
|
|
276
|
-
scheduledAt: toIso(interaction.scheduledAt),
|
|
277
|
-
occurredAt: toIso(interaction.occurredAt),
|
|
278
|
-
})),
|
|
279
|
-
tags: tagsRows
|
|
280
|
-
.map((tag) => {
|
|
281
|
-
if (!tag || typeof tag !== 'object') return null
|
|
282
|
-
const id = typeof tag.id === 'string' ? tag.id : null
|
|
283
|
-
const label = typeof tag.label === 'string' ? tag.label : null
|
|
284
|
-
if (!id || !label) return null
|
|
285
|
-
const slug = typeof tag.slug === 'string' ? tag.slug : label
|
|
286
|
-
const color = typeof tag.color === 'string' ? tag.color : null
|
|
287
|
-
return { id, slug, label, color }
|
|
288
|
-
})
|
|
289
|
-
.filter(
|
|
290
|
-
(entry): entry is { id: string; slug: string; label: string; color: string | null } =>
|
|
291
|
-
entry !== null,
|
|
292
|
-
),
|
|
293
|
-
deals: dealsRows
|
|
294
|
-
.map((deal) => {
|
|
295
|
-
if (!deal || typeof deal !== 'object') return null
|
|
296
|
-
const id = typeof deal.id === 'string' ? deal.id : null
|
|
297
|
-
if (!id) return null
|
|
298
|
-
return {
|
|
299
|
-
id,
|
|
300
|
-
title: typeof deal.title === 'string' ? deal.title : '',
|
|
301
|
-
status: typeof deal.status === 'string' ? deal.status : null,
|
|
302
|
-
pipelineStageId:
|
|
303
|
-
typeof deal.pipelineStageId === 'string' ? deal.pipelineStageId : null,
|
|
304
|
-
valueAmount:
|
|
305
|
-
typeof deal.valueAmount === 'string'
|
|
306
|
-
? deal.valueAmount
|
|
307
|
-
: deal.valueAmount === null || deal.valueAmount === undefined
|
|
308
|
-
? null
|
|
309
|
-
: String(deal.valueAmount),
|
|
310
|
-
valueCurrency:
|
|
311
|
-
typeof deal.valueCurrency === 'string' ? deal.valueCurrency : null,
|
|
312
|
-
}
|
|
313
|
-
})
|
|
314
|
-
.filter(
|
|
315
|
-
(
|
|
316
|
-
value,
|
|
317
|
-
): value is {
|
|
318
|
-
id: string
|
|
319
|
-
title: string
|
|
320
|
-
status: string | null
|
|
321
|
-
pipelineStageId: string | null
|
|
322
|
-
valueAmount: string | null
|
|
323
|
-
valueCurrency: string | null
|
|
324
|
-
} => value !== null,
|
|
325
|
-
),
|
|
326
|
-
}
|
|
204
|
+
related = buildRelatedRecords(data)
|
|
327
205
|
}
|
|
328
206
|
|
|
329
207
|
return {
|