@open-mercato/core 0.4.11-develop.1355.50152f3ee9 → 0.4.11-develop.1362.574a071900

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 (35) hide show
  1. package/dist/modules/customers/api/companies/route.js +141 -3
  2. package/dist/modules/customers/api/companies/route.js.map +2 -2
  3. package/dist/modules/customers/api/deals/route.js +52 -3
  4. package/dist/modules/customers/api/deals/route.js.map +2 -2
  5. package/dist/modules/customers/api/people/route.js +145 -3
  6. package/dist/modules/customers/api/people/route.js.map +2 -2
  7. package/dist/modules/customers/api/utils.js +195 -0
  8. package/dist/modules/customers/api/utils.js.map +2 -2
  9. package/dist/modules/customers/backend/customers/companies/page.js +171 -6
  10. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  11. package/dist/modules/customers/backend/customers/deals/page.js +100 -7
  12. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  13. package/dist/modules/customers/backend/customers/people/page.js +180 -7
  14. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  15. package/dist/modules/customers/commands/interactions.js +7 -0
  16. package/dist/modules/customers/commands/interactions.js.map +2 -2
  17. package/dist/modules/customers/components/detail/DealForm.js +1 -0
  18. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  19. package/dist/modules/query_index/lib/engine.js +81 -1
  20. package/dist/modules/query_index/lib/engine.js.map +2 -2
  21. package/package.json +3 -3
  22. package/src/modules/customers/api/companies/route.ts +151 -3
  23. package/src/modules/customers/api/deals/route.ts +54 -3
  24. package/src/modules/customers/api/people/route.ts +160 -3
  25. package/src/modules/customers/api/utils.ts +286 -0
  26. package/src/modules/customers/backend/customers/companies/page.tsx +184 -9
  27. package/src/modules/customers/backend/customers/deals/page.tsx +127 -35
  28. package/src/modules/customers/backend/customers/people/page.tsx +191 -10
  29. package/src/modules/customers/commands/interactions.ts +7 -0
  30. package/src/modules/customers/components/detail/DealForm.tsx +1 -0
  31. package/src/modules/customers/i18n/de.json +12 -0
  32. package/src/modules/customers/i18n/en.json +15 -3
  33. package/src/modules/customers/i18n/es.json +12 -0
  34. package/src/modules/customers/i18n/pl.json +12 -0
  35. package/src/modules/query_index/lib/engine.ts +95 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.4.11-develop.1355.50152f3ee9",
3
+ "version": "0.4.11-develop.1362.574a071900",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -230,10 +230,10 @@
230
230
  "ts-pattern": "^5.0.0"
231
231
  },
232
232
  "peerDependencies": {
233
- "@open-mercato/shared": "0.4.11-develop.1355.50152f3ee9"
233
+ "@open-mercato/shared": "0.4.11-develop.1362.574a071900"
234
234
  },
235
235
  "devDependencies": {
236
- "@open-mercato/shared": "0.4.11-develop.1355.50152f3ee9",
236
+ "@open-mercato/shared": "0.4.11-develop.1362.574a071900",
237
237
  "@testing-library/dom": "^10.4.1",
238
238
  "@testing-library/jest-dom": "^6.9.1",
239
239
  "@testing-library/react": "^16.3.1",
@@ -2,11 +2,17 @@
2
2
  import { z } from 'zod'
3
3
  import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
4
4
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
5
- import { CustomerEntity } from '../../data/entities'
5
+ import { CustomerCompanyProfile, CustomerEntity } from '../../data/entities'
6
6
  import { E } from '#generated/entities.ids.generated'
7
7
  import { companyCreateSchema, companyUpdateSchema } from '../../data/validators'
8
8
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
9
- import { withScopedPayload } from '../utils'
9
+ import {
10
+ applyEntityIdRestriction,
11
+ consumeAdvancedFilterState,
12
+ findMatchingEntityIdsWithQueryEngine,
13
+ findMatchingEntityIdsBySearchTokensAcrossSources,
14
+ withScopedPayload,
15
+ } from '../utils'
10
16
  import {
11
17
  buildCustomFieldFiltersFromQuery,
12
18
  extractAllCustomFieldEntries,
@@ -14,6 +20,8 @@ import {
14
20
  } from '@open-mercato/shared/lib/crud/custom-fields'
15
21
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
16
22
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
23
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
24
+ import { mergeAdvancedFilters } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
17
25
  import {
18
26
  createCustomersCrudOpenApi,
19
27
  createPagedListResponseSchema,
@@ -93,10 +101,67 @@ const crud = makeCrudRoute({
93
101
  updatedAt: 'updated_at',
94
102
  },
95
103
  buildFilters: async (query: any, ctx) => {
104
+ const advancedQuery = { ...query }
105
+ const advancedFilterState = consumeAdvancedFilterState(query)
96
106
  const filters: Record<string, any> = { kind: { $eq: 'company' } }
97
107
  if (query.id) filters.id = { $eq: query.id }
98
108
  if (query.search) {
99
- filters.display_name = { $ilike: `%${escapeLikePattern(query.search)}%` }
109
+ const matchingIds = ctx
110
+ ? await findMatchingEntityIdsBySearchTokensAcrossSources({
111
+ ctx,
112
+ query: query.search,
113
+ sources: [
114
+ {
115
+ entityType: E.customers.customer_entity,
116
+ fields: [
117
+ 'display_name',
118
+ 'primary_email',
119
+ 'primary_phone',
120
+ 'description',
121
+ 'status',
122
+ 'lifecycle_stage',
123
+ 'source',
124
+ 'next_interaction_name',
125
+ ],
126
+ },
127
+ {
128
+ entityType: E.customers.customer_company_profile,
129
+ fields: [
130
+ 'display_name',
131
+ 'primary_email',
132
+ 'primary_phone',
133
+ 'description',
134
+ 'status',
135
+ 'lifecycle_stage',
136
+ 'source',
137
+ 'legal_name',
138
+ 'brand_name',
139
+ 'domain',
140
+ 'website_url',
141
+ 'industry',
142
+ 'size_bucket',
143
+ 'annual_revenue',
144
+ ],
145
+ mapToEntityIds: {
146
+ table: 'customer_companies',
147
+ targetColumn: 'entity_id',
148
+ },
149
+ },
150
+ ],
151
+ })
152
+ : null
153
+ if (matchingIds !== null && matchingIds.length > 0) {
154
+ applyEntityIdRestriction(filters, matchingIds)
155
+ } else {
156
+ const searchPattern = `%${escapeLikePattern(query.search)}%`
157
+ filters.$or = [
158
+ { display_name: { $ilike: searchPattern } },
159
+ { primary_email: { $ilike: searchPattern } },
160
+ { primary_phone: { $ilike: searchPattern } },
161
+ { description: { $ilike: searchPattern } },
162
+ { next_interaction_name: { $ilike: searchPattern } },
163
+ ]
164
+ }
100
165
  }
101
166
  if (query.status) {
102
167
  filters.status = { $eq: query.status }
@@ -166,6 +231,36 @@ const crud = makeCrudRoute({
166
231
  // ignore custom field filter errors; fall back to base filters
167
232
  }
168
233
  }
234
+ if (ctx && advancedFilterState) {
235
+ const advancedFilters = mergeAdvancedFilters(
236
+ { ...filters },
237
+ advancedQuery as Record<string, unknown>,
238
+ )
239
+ const matchedIds = await findMatchingEntityIdsWithQueryEngine({
240
+ ctx,
241
+ entityId: E.customers.customer_entity,
242
+ filters: advancedFilters,
243
+ customFieldSources: [
244
+ {
245
+ entityId: E.customers.customer_company_profile,
246
+ table: 'customer_companies',
247
+ alias: 'company_profile',
248
+ recordIdColumn: 'id',
249
+ join: { fromField: 'id', toField: 'entity_id' },
250
+ },
251
+ ],
252
+ joins: [
253
+ {
254
+ alias: 'tag_assignments',
255
+ table: 'customer_tag_assignments',
256
+ from: { field: 'id' },
257
+ to: { field: 'entity_id' },
258
+ type: 'left',
259
+ },
260
+ ],
261
+ })
262
+ applyEntityIdRestriction(filters, matchedIds)
263
+ }
169
264
  return filters
170
265
  },
171
266
  customFieldSources: [
@@ -244,6 +339,59 @@ const crud = makeCrudRoute({
244
339
  response: () => ({ ok: true }),
245
340
  },
246
341
  },
342
+ hooks: {
343
+ afterList: async (payload, ctx) => {
344
+ const items = Array.isArray(payload?.items) ? payload.items : []
345
+ const ids = items
346
+ .map((item: unknown) => (item && typeof item === 'object' && typeof (item as Record<string, unknown>).id === 'string'
347
+ ? (item as Record<string, unknown>).id as string
348
+ : null))
349
+ .filter((id: string | null): id is string => typeof id === 'string' && id.length > 0)
350
+ if (!ids.length) return
351
+
352
+ const where: Record<string, unknown> = {
353
+ entity: { $in: ids },
354
+ tenantId: ctx.auth?.tenantId ?? null,
355
+ }
356
+ if (ctx.selectedOrganizationId) {
357
+ where.organizationId = ctx.selectedOrganizationId
358
+ }
359
+
360
+ const profiles = await findWithDecryption(
361
+ ctx.container.resolve('em') as any,
362
+ CustomerCompanyProfile,
363
+ where as any,
364
+ { populate: ['entity'] } as any,
365
+ {
366
+ tenantId: ctx.auth?.tenantId ?? null,
367
+ organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
368
+ },
369
+ )
370
+
371
+ const profilesByEntityId = new Map<string, CustomerCompanyProfile>()
372
+ for (const profile of profiles) {
373
+ const entityId = typeof (profile as any)?.entity?.id === 'string' ? (profile as any).entity.id : null
374
+ if (entityId) profilesByEntityId.set(entityId, profile)
375
+ }
376
+
377
+ payload.items = items.map((item: unknown) => {
378
+ if (!item || typeof item !== 'object') return item
379
+ const record = item as Record<string, unknown>
380
+ const profile = typeof record.id === 'string' ? profilesByEntityId.get(record.id) : undefined
381
+ if (!profile) return item
382
+ return {
383
+ ...record,
384
+ legal_name: profile.legalName ?? null,
385
+ brand_name: profile.brandName ?? null,
386
+ domain: profile.domain ?? null,
387
+ website_url: profile.websiteUrl ?? null,
388
+ industry: profile.industry ?? null,
389
+ size_bucket: profile.sizeBucket ?? null,
390
+ annual_revenue: profile.annualRevenue ?? null,
391
+ }
392
+ })
393
+ },
394
+ },
247
395
  })
248
396
 
249
397
  const { POST, PUT, DELETE } = crud
@@ -6,7 +6,13 @@ import { CustomerDeal, CustomerDealPersonLink, CustomerDealCompanyLink } from '.
6
6
  import { dealCreateSchema, dealUpdateSchema } from '../../data/validators'
7
7
  import { E } from '#generated/entities.ids.generated'
8
8
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
9
- import { parseScopedCommandInput } from '../utils'
9
+ import {
10
+ applyEntityIdRestriction,
11
+ consumeAdvancedFilterState,
12
+ findMatchingEntityIdsWithQueryEngine,
13
+ findMatchingEntityIdsBySearchTokensAcrossSources,
14
+ parseScopedCommandInput,
15
+ } from '../utils'
10
16
  import type { EntityManager } from '@mikro-orm/postgresql'
11
17
  import {
12
18
  createCustomersCrudOpenApi,
@@ -15,6 +21,7 @@ import {
15
21
  } from '../openapi'
16
22
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
17
23
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
24
+ import { mergeAdvancedFilters } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
18
25
 
19
26
  const rawBodySchema = z.object({}).passthrough()
20
27
 
@@ -122,10 +129,42 @@ const crud = makeCrudRoute<unknown, unknown, DealListQuery>({
122
129
  title: 'title',
123
130
  value: 'value_amount',
124
131
  },
125
- buildFilters: async (query: any) => {
132
+ buildFilters: async (query: any, ctx) => {
133
+ const advancedQuery = { ...query }
134
+ const advancedFilterState = consumeAdvancedFilterState(query)
126
135
  const filters: Record<string, any> = {}
127
136
  if (query.search) {
128
- filters.title = { $ilike: `%${escapeLikePattern(query.search)}%` }
137
+ const matchingIds = ctx
138
+ ? await findMatchingEntityIdsBySearchTokensAcrossSources({
139
+ ctx,
140
+ query: query.search,
141
+ sources: [
142
+ {
143
+ entityType: E.customers.customer_deal,
144
+ fields: [
145
+ 'title',
146
+ 'description',
147
+ 'status',
148
+ 'pipeline_stage',
149
+ 'source',
150
+ 'value_amount',
151
+ 'value_currency',
152
+ 'cf:competitive_risk',
153
+ 'cf:implementation_complexity',
154
+ ],
155
+ },
156
+ ],
157
+ })
158
+ : null
159
+ if (matchingIds !== null && matchingIds.length > 0) {
160
+ applyEntityIdRestriction(filters, matchingIds)
161
+ } else {
162
+ const searchPattern = `%${escapeLikePattern(query.search)}%`
163
+ filters.$or = [
164
+ { title: { $ilike: searchPattern } },
165
+ { description: { $ilike: searchPattern } },
166
+ ]
167
+ }
129
168
  }
130
169
  if (query.status) {
131
170
  filters.status = { $eq: query.status }
@@ -139,6 +178,18 @@ const crud = makeCrudRoute<unknown, unknown, DealListQuery>({
139
178
  if (query.pipelineStageId) {
140
179
  filters.pipeline_stage_id = { $eq: query.pipelineStageId }
141
180
  }
181
+ if (ctx && advancedFilterState) {
182
+ const advancedFilters = mergeAdvancedFilters(
183
+ { ...filters },
184
+ advancedQuery as Record<string, unknown>,
185
+ )
186
+ const matchedIds = await findMatchingEntityIdsWithQueryEngine({
187
+ ctx,
188
+ entityId: E.customers.customer_deal,
189
+ filters: advancedFilters,
190
+ })
191
+ applyEntityIdRestriction(filters, matchedIds)
192
+ }
142
193
  return filters
143
194
  },
144
195
  },
@@ -2,14 +2,22 @@
2
2
  import { z } from 'zod'
3
3
  import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
4
4
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
5
- import { CustomerEntity } from '../../data/entities'
5
+ import { CustomerEntity, CustomerPersonProfile } from '../../data/entities'
6
6
  import { E } from '#generated/entities.ids.generated'
7
7
  import { personCreateSchema, personUpdateSchema } from '../../data/validators'
8
8
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
9
- import { withScopedPayload } from '../utils'
9
+ import {
10
+ applyEntityIdRestriction,
11
+ consumeAdvancedFilterState,
12
+ findMatchingEntityIdsWithQueryEngine,
13
+ findMatchingEntityIdsBySearchTokensAcrossSources,
14
+ withScopedPayload,
15
+ } from '../utils'
10
16
  import { buildCustomFieldFiltersFromQuery, extractAllCustomFieldEntries, splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'
11
17
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
12
18
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
19
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
20
+ import { mergeAdvancedFilters } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
13
21
  import {
14
22
  createCustomersCrudOpenApi,
15
23
  createPagedListResponseSchema,
@@ -90,10 +98,68 @@ const crud = makeCrudRoute({
90
98
  updatedAt: 'updated_at',
91
99
  },
92
100
  buildFilters: async (query: any, ctx) => {
101
+ const advancedQuery = { ...query }
102
+ const advancedFilterState = consumeAdvancedFilterState(query)
93
103
  const filters: Record<string, any> = { kind: { $eq: 'person' } }
94
104
  if (query.id) filters.id = { $eq: query.id }
95
105
  if (query.search) {
96
- filters.display_name = { $ilike: `%${escapeLikePattern(query.search)}%` }
106
+ const matchingIds = ctx
107
+ ? await findMatchingEntityIdsBySearchTokensAcrossSources({
108
+ ctx,
109
+ query: query.search,
110
+ sources: [
111
+ {
112
+ entityType: E.customers.customer_entity,
113
+ fields: [
114
+ 'display_name',
115
+ 'primary_email',
116
+ 'primary_phone',
117
+ 'description',
118
+ 'status',
119
+ 'lifecycle_stage',
120
+ 'source',
121
+ 'next_interaction_name',
122
+ ],
123
+ },
124
+ {
125
+ entityType: E.customers.customer_person_profile,
126
+ fields: [
127
+ 'display_name',
128
+ 'primary_email',
129
+ 'primary_phone',
130
+ 'status',
131
+ 'lifecycle_stage',
132
+ 'source',
133
+ 'first_name',
134
+ 'last_name',
135
+ 'preferred_name',
136
+ 'job_title',
137
+ 'department',
138
+ 'seniority',
139
+ 'timezone',
140
+ 'linked_in_url',
141
+ 'twitter_url',
142
+ ],
143
+ mapToEntityIds: {
144
+ table: 'customer_people',
145
+ targetColumn: 'entity_id',
146
+ },
147
+ },
148
+ ],
149
+ })
150
+ : null
151
+ if (matchingIds !== null && matchingIds.length > 0) {
152
+ applyEntityIdRestriction(filters, matchingIds)
153
+ } else {
154
+ const searchPattern = `%${escapeLikePattern(query.search)}%`
155
+ filters.$or = [
156
+ { display_name: { $ilike: searchPattern } },
157
+ { primary_email: { $ilike: searchPattern } },
158
+ { primary_phone: { $ilike: searchPattern } },
159
+ { description: { $ilike: searchPattern } },
160
+ { next_interaction_name: { $ilike: searchPattern } },
161
+ ]
162
+ }
97
163
  }
98
164
  const email = typeof query.email === 'string' ? query.email.trim().toLowerCase() : ''
99
165
  const emailStartsWith = typeof query.emailStartsWith === 'string' ? query.emailStartsWith.trim().toLowerCase() : ''
@@ -163,6 +229,36 @@ const crud = makeCrudRoute({
163
229
  // ignore custom field filter errors; fall back to base filters
164
230
  }
165
231
  }
232
+ if (ctx && advancedFilterState) {
233
+ const advancedFilters = mergeAdvancedFilters(
234
+ { ...filters },
235
+ advancedQuery as Record<string, unknown>,
236
+ )
237
+ const matchedIds = await findMatchingEntityIdsWithQueryEngine({
238
+ ctx,
239
+ entityId: E.customers.customer_entity,
240
+ filters: advancedFilters,
241
+ customFieldSources: [
242
+ {
243
+ entityId: E.customers.customer_person_profile,
244
+ table: 'customer_people',
245
+ alias: 'person_profile',
246
+ recordIdColumn: 'id',
247
+ join: { fromField: 'id', toField: 'entity_id' },
248
+ },
249
+ ],
250
+ joins: [
251
+ {
252
+ alias: 'tag_assignments',
253
+ table: 'customer_tag_assignments',
254
+ from: { field: 'id' },
255
+ to: { field: 'entity_id' },
256
+ type: 'left',
257
+ },
258
+ ],
259
+ })
260
+ applyEntityIdRestriction(filters, matchedIds)
261
+ }
166
262
  return filters
167
263
  },
168
264
  customFieldSources: [
@@ -241,6 +337,67 @@ const crud = makeCrudRoute({
241
337
  response: () => ({ ok: true }),
242
338
  },
243
339
  },
340
+ hooks: {
341
+ afterList: async (payload, ctx) => {
342
+ const items = Array.isArray(payload?.items) ? payload.items : []
343
+ const ids = items
344
+ .map((item: unknown) => (
345
+ item && typeof item === 'object' && typeof (item as Record<string, unknown>).id === 'string'
346
+ ? (item as Record<string, unknown>).id as string
347
+ : null
348
+ ))
349
+ .filter((id: string | null): id is string => typeof id === 'string' && id.length > 0)
350
+ if (!ids.length) return
351
+
352
+ const where: Record<string, unknown> = {
353
+ entity: { $in: ids },
354
+ tenantId: ctx.auth?.tenantId ?? null,
355
+ }
356
+ if (ctx.selectedOrganizationId) {
357
+ where.organizationId = ctx.selectedOrganizationId
358
+ }
359
+
360
+ const profiles = await findWithDecryption(
361
+ ctx.container.resolve('em') as any,
362
+ CustomerPersonProfile,
363
+ where as any,
364
+ { populate: ['entity', 'company'] } as any,
365
+ {
366
+ tenantId: ctx.auth?.tenantId ?? null,
367
+ organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
368
+ },
369
+ )
370
+
371
+ const profilesByEntityId = new Map<string, CustomerPersonProfile>()
372
+ for (const profile of profiles) {
373
+ const entityId = typeof (profile as any)?.entity?.id === 'string' ? (profile as any).entity.id : null
374
+ if (entityId) profilesByEntityId.set(entityId, profile)
375
+ }
376
+
377
+ payload.items = items.map((item: unknown) => {
378
+ if (!item || typeof item !== 'object') return item
379
+ const record = item as Record<string, unknown>
380
+ const profile = typeof record.id === 'string' ? profilesByEntityId.get(record.id) : undefined
381
+ if (!profile) return item
382
+ return {
383
+ ...record,
384
+ first_name: profile.firstName ?? null,
385
+ last_name: profile.lastName ?? null,
386
+ preferred_name: profile.preferredName ?? null,
387
+ job_title: profile.jobTitle ?? null,
388
+ department: profile.department ?? null,
389
+ seniority: profile.seniority ?? null,
390
+ timezone: profile.timezone ?? null,
391
+ linked_in_url: profile.linkedInUrl ?? null,
392
+ twitter_url: profile.twitterUrl ?? null,
393
+ company_entity_id:
394
+ profile.company && typeof profile.company === 'object'
395
+ ? profile.company.id
396
+ : profile.company ?? null,
397
+ }
398
+ })
399
+ },
400
+ },
244
401
  })
245
402
 
246
403
  const { POST, PUT, DELETE } = crud