@open-mercato/core 0.6.7-develop.6706.1.b3a4c759bb → 0.6.7-develop.6726.1.983ae8a07e

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 (117) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/admin/nav.js +14 -4
  3. package/dist/modules/auth/api/admin/nav.js.map +2 -2
  4. package/dist/modules/auth/api/users/route.js +8 -19
  5. package/dist/modules/auth/api/users/route.js.map +2 -2
  6. package/dist/modules/auth/lib/backendChrome.js +31 -14
  7. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  8. package/dist/modules/customers/api/utils.js +12 -18
  9. package/dist/modules/customers/api/utils.js.map +2 -2
  10. package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +17 -3
  11. package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +2 -2
  12. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +7 -1
  13. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  14. package/dist/modules/customers/cli.js +11 -4
  15. package/dist/modules/customers/cli.js.map +2 -2
  16. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +4 -2
  17. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  18. package/dist/modules/customers/components/formConfig.js +14 -10
  19. package/dist/modules/customers/components/formConfig.js.map +2 -2
  20. package/dist/modules/dashboards/api/widgets/data/batch/route.js +4 -2
  21. package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +2 -2
  22. package/dist/modules/dashboards/api/widgets/data/route.js +20 -2
  23. package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
  24. package/dist/modules/dashboards/lib/aggregations.js +76 -40
  25. package/dist/modules/dashboards/lib/aggregations.js.map +2 -2
  26. package/dist/modules/dashboards/lib/exactDecimal.js +69 -0
  27. package/dist/modules/dashboards/lib/exactDecimal.js.map +7 -0
  28. package/dist/modules/dashboards/services/widgetDataService.js +228 -5
  29. package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
  30. package/dist/modules/feature_toggles/components/FeatureGuard.js +3 -2
  31. package/dist/modules/feature_toggles/components/FeatureGuard.js.map +2 -2
  32. package/dist/modules/feature_toggles/components/hooks/staleTime.js +5 -0
  33. package/dist/modules/feature_toggles/components/hooks/staleTime.js.map +7 -0
  34. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js +5 -2
  35. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js.map +2 -2
  36. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js +5 -2
  37. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js.map +2 -2
  38. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js +5 -2
  39. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js.map +2 -2
  40. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js +5 -2
  41. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js.map +2 -2
  42. package/dist/modules/inbox_ops/api/proposals/route.js +16 -1
  43. package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
  44. package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js +32 -0
  45. package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js.map +7 -0
  46. package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js +0 -5
  47. package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js.map +2 -2
  48. package/dist/modules/integrations/widgets/injection/external-ids/widget.js +14 -0
  49. package/dist/modules/integrations/widgets/injection/external-ids/widget.js.map +7 -0
  50. package/dist/modules/messages/lib/searchLookup.js +8 -16
  51. package/dist/modules/messages/lib/searchLookup.js.map +2 -2
  52. package/dist/modules/query_index/api/openapi.js +13 -1
  53. package/dist/modules/query_index/api/openapi.js.map +2 -2
  54. package/dist/modules/query_index/api/status.js +165 -105
  55. package/dist/modules/query_index/api/status.js.map +2 -2
  56. package/dist/modules/query_index/components/QueryIndexesTable.js +128 -41
  57. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  58. package/dist/modules/query_index/lib/batch.js +11 -4
  59. package/dist/modules/query_index/lib/batch.js.map +2 -2
  60. package/dist/modules/query_index/lib/document.js +12 -5
  61. package/dist/modules/query_index/lib/document.js.map +2 -2
  62. package/dist/modules/query_index/lib/engine.js +36 -4
  63. package/dist/modules/query_index/lib/engine.js.map +2 -2
  64. package/dist/modules/query_index/lib/indexer.js +1 -1
  65. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  66. package/dist/modules/query_index/lib/search-tokens.js +7 -4
  67. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  68. package/dist/modules/sales/api/documents/factory.js +14 -3
  69. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  70. package/dist/modules/sales/commands/documents.js +22 -1
  71. package/dist/modules/sales/commands/documents.js.map +2 -2
  72. package/dist/modules/sales/components/useSalesChannelsEnabled.js +2 -35
  73. package/dist/modules/sales/components/useSalesChannelsEnabled.js.map +2 -2
  74. package/dist/modules/sales/data/validators.js +25 -4
  75. package/dist/modules/sales/data/validators.js.map +2 -2
  76. package/package.json +7 -7
  77. package/src/modules/auth/api/admin/nav.ts +24 -4
  78. package/src/modules/auth/api/users/route.ts +8 -28
  79. package/src/modules/auth/lib/backendChrome.tsx +51 -13
  80. package/src/modules/customers/api/utils.ts +12 -31
  81. package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +35 -3
  82. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +10 -1
  83. package/src/modules/customers/cli.ts +11 -4
  84. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +4 -0
  85. package/src/modules/customers/components/formConfig.tsx +12 -8
  86. package/src/modules/dashboards/api/widgets/data/batch/route.ts +4 -0
  87. package/src/modules/dashboards/api/widgets/data/route.ts +21 -0
  88. package/src/modules/dashboards/lib/aggregations.ts +122 -43
  89. package/src/modules/dashboards/lib/exactDecimal.ts +96 -0
  90. package/src/modules/dashboards/services/widgetDataService.ts +342 -4
  91. package/src/modules/feature_toggles/components/FeatureGuard.tsx +9 -2
  92. package/src/modules/feature_toggles/components/hooks/staleTime.ts +5 -0
  93. package/src/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.ts +13 -1
  94. package/src/modules/feature_toggles/components/hooks/useFeatureFlagJson.ts +12 -3
  95. package/src/modules/feature_toggles/components/hooks/useFeatureFlagNumber.ts +10 -1
  96. package/src/modules/feature_toggles/components/hooks/useFeatureFlagString.ts +10 -1
  97. package/src/modules/inbox_ops/api/proposals/route.ts +20 -1
  98. package/src/modules/inbox_ops/subscribers/search-reindex-proposal-created.ts +39 -0
  99. package/src/modules/integrations/widgets/injection/external-ids/widget.client.tsx +0 -5
  100. package/src/modules/integrations/widgets/injection/external-ids/widget.ts +13 -0
  101. package/src/modules/messages/lib/searchLookup.ts +11 -34
  102. package/src/modules/query_index/api/openapi.ts +12 -0
  103. package/src/modules/query_index/api/status.ts +238 -136
  104. package/src/modules/query_index/components/QueryIndexesTable.tsx +192 -46
  105. package/src/modules/query_index/i18n/de.json +5 -3
  106. package/src/modules/query_index/i18n/en.json +5 -3
  107. package/src/modules/query_index/i18n/es.json +5 -3
  108. package/src/modules/query_index/i18n/pl.json +5 -3
  109. package/src/modules/query_index/lib/batch.ts +11 -4
  110. package/src/modules/query_index/lib/document.ts +35 -4
  111. package/src/modules/query_index/lib/engine.ts +53 -5
  112. package/src/modules/query_index/lib/indexer.ts +1 -1
  113. package/src/modules/query_index/lib/search-tokens.ts +13 -4
  114. package/src/modules/sales/api/documents/factory.ts +28 -3
  115. package/src/modules/sales/commands/documents.ts +26 -2
  116. package/src/modules/sales/components/useSalesChannelsEnabled.ts +2 -48
  117. package/src/modules/sales/data/validators.ts +50 -5
@@ -22,6 +22,7 @@ import { resolveRegisteredLucideIconNode } from '@open-mercato/ui/backend/icons/
22
22
  import { profilePathPrefixes, profileSections } from './profile-sections'
23
23
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
24
24
  import { filterGrantsByEnabledModules } from '@open-mercato/shared/security/enabledModulesRegistry'
25
+ import { getNavGroupOrderOverride } from '@open-mercato/shared/modules/overrides'
25
26
  import {
26
27
  getSelectedOrganizationFromRequest,
27
28
  resolveFeatureCheckContext,
@@ -147,19 +148,35 @@ async function serializeNavItem(item: AdminNavItem): Promise<ResolvedNavItem> {
147
148
  }
148
149
  }
149
150
 
151
+ const defaultGroupOrder = [
152
+ 'customers.nav.group',
153
+ 'catalog.nav.group',
154
+ 'customers~sales.nav.group',
155
+ 'wms.nav.group',
156
+ 'resources.nav.group',
157
+ 'staff.nav.group',
158
+ 'entities.nav.group',
159
+ 'directory.nav.group',
160
+ 'attachments.nav.group',
161
+ ]
162
+
163
+ /**
164
+ * Group ids ranked ahead of everything else, most significant first.
165
+ *
166
+ * An app may prepend its own ids via `overrides.nav.groupOrder` in `modules.ts`; ids it does not name
167
+ * keep the ordering they have today. With no override configured this returns `defaultGroupOrder`
168
+ * itself, so ordering is unchanged for every existing install.
169
+ */
170
+ function resolveGroupOrder(): string[] {
171
+ const override = getNavGroupOrderOverride()
172
+ if (!override || override.length === 0) return defaultGroupOrder
173
+ const overridden = new Set(override)
174
+ return [...override, ...defaultGroupOrder.filter((id) => !overridden.has(id))]
175
+ }
176
+
150
177
  function normalizeGroupWeights(groups: NavGroupWithWeight[]): NavGroupWithWeight[] {
151
- const defaultGroupOrder = [
152
- 'customers.nav.group',
153
- 'catalog.nav.group',
154
- 'customers~sales.nav.group',
155
- 'wms.nav.group',
156
- 'resources.nav.group',
157
- 'staff.nav.group',
158
- 'entities.nav.group',
159
- 'directory.nav.group',
160
- 'attachments.nav.group',
161
- ]
162
- const groupOrderIndex = new Map(defaultGroupOrder.map((id, index) => [id, index]))
178
+ const groupOrder = resolveGroupOrder()
179
+ const groupOrderIndex = new Map(groupOrder.map((id, index) => [id, index]))
163
180
  groups.sort((a, b) => {
164
181
  const aIndex = groupOrderIndex.get(a.id)
165
182
  const bIndex = groupOrderIndex.get(b.id)
@@ -171,7 +188,7 @@ function normalizeGroupWeights(groups: NavGroupWithWeight[]): NavGroupWithWeight
171
188
  if (a.weight !== b.weight) return a.weight - b.weight
172
189
  return a.name.localeCompare(b.name)
173
190
  })
174
- const defaultGroupCount = defaultGroupOrder.length
191
+ const defaultGroupCount = groupOrder.length
175
192
  groups.forEach((group, index) => {
176
193
  const rank = groupOrderIndex.get(group.id)
177
194
  const fallbackWeight = typeof group.weight === 'number' ? group.weight : 10_000
@@ -275,6 +292,11 @@ export async function resolveBackendChromePayload({
275
292
 
276
293
  let scopedOrganizationId: string | null = auth.orgId ?? null
277
294
  let scopedTenantId: string | null = auth.tenantId ?? null
295
+ // The organization the caller actually *selected*, as distinct from the one the scope resolver fell
296
+ // back to. `resolveFeatureCheckContext` resolves `organizationId` to `auth.orgId` when no concrete
297
+ // organization is selected — which is precisely what an all-organizations view produces — so the
298
+ // resolved id cannot answer "which organization am I viewing".
299
+ let concretelySelectedOrganizationId: string | null = null
278
300
  let allowNavigation = true
279
301
 
280
302
  try {
@@ -287,12 +309,14 @@ export async function resolveBackendChromePayload({
287
309
  })
288
310
  scopedOrganizationId = organizationId
289
311
  scopedTenantId = scope.tenantId ?? auth.tenantId ?? null
312
+ concretelySelectedOrganizationId = scope.selectedId ?? null
290
313
  if (Array.isArray(allowedOrganizationIds) && allowedOrganizationIds.length === 0) {
291
314
  allowNavigation = false
292
315
  }
293
316
  } catch {
294
317
  scopedOrganizationId = auth.orgId ?? null
295
318
  scopedTenantId = auth.tenantId ?? null
319
+ concretelySelectedOrganizationId = null
296
320
  }
297
321
 
298
322
  const acl = allowNavigation
@@ -411,6 +435,11 @@ export async function resolveBackendChromePayload({
411
435
  ?? (fallbackOrganizationId && !isAllOrganizationsSelection(fallbackOrganizationId) ? fallbackOrganizationId : null)
412
436
 
413
437
  let brand: BackendChromePayload['brand'] = null
438
+ // Resolved here rather than left to callers. `brand` only populates when the organization has a
439
+ // logo, so it is a branding channel, not a dependable "which organization am I viewing" source.
440
+ // Without this field every downstream app has to fetch `/api/directory/organization-switcher` and
441
+ // walk its tree for the selected id. The row is already loaded below, so the name costs nothing.
442
+ let currentOrganization: BackendChromePayload['currentOrganization'] = null
414
443
  if (brandOrganizationId && scopedTenantId) {
415
444
  try {
416
445
  const organization = await findOneWithDecryption(
@@ -420,6 +449,12 @@ export async function resolveBackendChromePayload({
420
449
  undefined,
421
450
  { tenantId: scopedTenantId, organizationId: brandOrganizationId },
422
451
  )
452
+ // Only when a concrete organization was selected. Under an all-organizations view
453
+ // `brandOrganizationId` still resolves (to the caller's own organization, which is what keeps
454
+ // branding working), so gating on the loaded row alone would misreport the scope.
455
+ if (organization && concretelySelectedOrganizationId === brandOrganizationId) {
456
+ currentOrganization = { id: String(organization.id), name: organization.name }
457
+ }
423
458
  if (organization?.logoUrl) {
424
459
  brand = {
425
460
  name: organization.name,
@@ -430,7 +465,9 @@ export async function resolveBackendChromePayload({
430
465
  }
431
466
  }
432
467
  } catch {
468
+ // Fail soft, as before: a failed organization lookup must not take down the nav payload.
433
469
  brand = null
470
+ currentOrganization = null
434
471
  }
435
472
  }
436
473
 
@@ -443,5 +480,6 @@ export async function resolveBackendChromePayload({
443
480
  grantedFeatures,
444
481
  roles: Array.isArray(auth.roles) ? auth.roles : [],
445
482
  brand,
483
+ currentOrganization,
446
484
  }
447
485
  }
@@ -4,8 +4,7 @@ import { sql } from 'kysely'
4
4
  import type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'
5
5
  import type { EntityId } from '@open-mercato/shared/modules/entities'
6
6
  import type { QueryCustomFieldSource, QueryJoinEdge, QueryEngine } from '@open-mercato/shared/lib/query/types'
7
- import { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'
8
- import { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'
7
+ import { findEntityIdsBySearchTokensCompat, type SearchTokenDatabase } from '@open-mercato/shared/lib/search/tokenLookup'
9
8
  import { SortDir } from '@open-mercato/shared/lib/query/types'
10
9
 
11
10
  const { withScopedPayload, parseScopedCommandInput } = createScopedApiHelpers({
@@ -106,36 +105,18 @@ async function findSearchTokenEntityIds({
106
105
  fields,
107
106
  query,
108
107
  }: SearchTokenMatchInput): Promise<string[] | null> {
109
- const trimmed = query.trim()
110
- if (!trimmed) return null
111
-
112
- const tokens = tokenizeText(trimmed, resolveSearchConfig())
113
- if (!tokens.hashes.length) return []
114
-
115
108
  const em = ctx.container.resolve('em') as EntityManager
116
- const db = em.getKysely<any>() as any
117
- let searchQuery = db
118
- .selectFrom('search_tokens')
119
- .select('entity_id')
120
- .where('entity_type', '=', entityType)
121
- .where('field', 'in', fields)
122
- .where('token_hash', 'in', tokens.hashes)
123
- .groupBy('entity_id')
124
- .having(sql<boolean>`count(distinct token_hash) >= ${tokens.hashes.length}`)
125
-
126
- if (ctx.auth?.tenantId !== undefined) {
127
- searchQuery = searchQuery.where(sql<boolean>`tenant_id is not distinct from ${ctx.auth?.tenantId ?? null}`)
128
- }
129
- if (ctx.selectedOrganizationId) {
130
- searchQuery = searchQuery.where('organization_id', '=', ctx.selectedOrganizationId)
131
- } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {
132
- searchQuery = searchQuery.where('organization_id', 'in', ctx.organizationIds)
133
- }
134
-
135
- const rows = await searchQuery.execute() as Array<{ entity_id?: unknown }>
136
- return rows
137
- .map((row) => (typeof row.entity_id === 'string' ? row.entity_id : null))
138
- .filter((id): id is string => typeof id === 'string' && id.length > 0)
109
+ return findEntityIdsBySearchTokensCompat({
110
+ db: em.getKysely<SearchTokenDatabase>(),
111
+ entityType,
112
+ query,
113
+ fields,
114
+ scope: {
115
+ tenantId: ctx.auth?.tenantId !== undefined ? ctx.auth?.tenantId ?? null : undefined,
116
+ organizationId: ctx.selectedOrganizationId ?? undefined,
117
+ organizationIds: ctx.organizationIds,
118
+ },
119
+ })
139
120
  }
140
121
 
141
122
  async function mapScopedEntityIds({
@@ -18,6 +18,7 @@ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuarde
18
18
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
19
19
  import { useT } from '@open-mercato/shared/lib/i18n/context'
20
20
  import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
21
+ import { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'
21
22
 
22
23
  // Deal status values written by the kanban flow. These intentionally do NOT include 'closed'
23
24
  // because the rest of the app only persists 'loose' for lost deals (see StatusFilterPopover
@@ -56,7 +57,18 @@ export type QuickDealCompanyOption = {
56
57
  label: string
57
58
  }
58
59
 
59
- type QuickDealDialogProps = {
60
+ /**
61
+ * Registered component id for the kanban quick-add dialog. Downstream apps can
62
+ * target it from `widgets/components.ts` to replace/wrap the dialog or to
63
+ * transform its props — e.g. `defaultProbability: null` so quick-create stops
64
+ * seeding a probability that overrides pipeline automation (#4329).
65
+ */
66
+ export const QUICK_DEAL_DIALOG_COMPONENT_ID = 'dialog:customers.deals.quickDeal'
67
+
68
+ /** Probability seeded into the quick-add form when the host passes no override. */
69
+ export const DEFAULT_QUICK_DEAL_PROBABILITY = 25
70
+
71
+ export type QuickDealDialogProps = {
60
72
  open: boolean
61
73
  context: QuickDealContext | null
62
74
  onClose: () => void
@@ -64,6 +76,13 @@ type QuickDealDialogProps = {
64
76
  currentUserId?: string
65
77
  currentUserLabel?: string
66
78
  companies?: QuickDealCompanyOption[]
79
+ /**
80
+ * Probability pre-filled in the collapsed "More details" group. `null` leaves
81
+ * the field empty, so quick-create sends no `probability` at all and whatever
82
+ * the pipeline/automation derives for the target stage stands. Defaults to
83
+ * `DEFAULT_QUICK_DEAL_PROBABILITY` to preserve the historic behavior.
84
+ */
85
+ defaultProbability?: number | null
67
86
  /**
68
87
  * Tenant currency list. When omitted (e.g. dictionary not yet loaded), we render a
69
88
  * conservative fallback so the dialog still works end-to-end. The default selection is
@@ -94,6 +113,7 @@ export function QuickDealDialog({
94
113
  currentUserLabel,
95
114
  companies = [],
96
115
  currencies,
116
+ defaultProbability = DEFAULT_QUICK_DEAL_PROBABILITY,
97
117
  }: QuickDealDialogProps): React.ReactElement | null {
98
118
  const t = useT()
99
119
  // Re-mount CrudForm whenever the dialog opens so cleared state is consistently fresh.
@@ -157,11 +177,11 @@ export function QuickDealDialog({
157
177
  valueCurrency: defaultCurrencyCode,
158
178
  companyId: '',
159
179
  status: 'open',
160
- probability: 25,
180
+ probability: defaultProbability,
161
181
  expectedCloseAt: '',
162
182
  description: '',
163
183
  }),
164
- [defaultCurrencyCode],
184
+ [defaultCurrencyCode, defaultProbability],
165
185
  )
166
186
 
167
187
  const fields = React.useMemo<CrudField[]>(
@@ -405,4 +425,16 @@ export function QuickDealDialog({
405
425
  )
406
426
  }
407
427
 
428
+ // Register so downstream apps can reach the dialog from `widgets/components.ts`
429
+ // (replace / wrap / propsTransform) instead of forking the whole kanban page
430
+ // just to change a default (#4329).
431
+ registerComponent<QuickDealDialogProps>({
432
+ id: QUICK_DEAL_DIALOG_COMPONENT_ID,
433
+ component: QuickDealDialog,
434
+ metadata: {
435
+ module: 'customers',
436
+ description: 'Kanban quick-add deal dialog (pipeline board "+ deal").',
437
+ },
438
+ })
439
+
408
440
  export default QuickDealDialog
@@ -64,9 +64,11 @@ import { CurrencyFilterPopover } from './components/CurrencyFilterPopover'
64
64
  import { AddStageLane } from './components/AddStageLane'
65
65
  import type { DealCardData } from './components/DealCard'
66
66
  import {
67
- QuickDealDialog,
67
+ QuickDealDialog as DefaultQuickDealDialog,
68
+ QUICK_DEAL_DIALOG_COMPONENT_ID,
68
69
  type QuickDealContext,
69
70
  type QuickDealCompanyOption,
71
+ type QuickDealDialogProps,
70
72
  } from './components/QuickDealDialog'
71
73
  import { AddStageDialog, type AddStageContext } from './components/AddStageDialog'
72
74
  import { StatusFilterPopover } from './components/StatusFilterPopover'
@@ -80,6 +82,7 @@ import {
80
82
  type ActivityComposerContext,
81
83
  } from './components/ActivityComposerDialog'
82
84
  import { BulkActionsBar } from './components/BulkActionsBar'
85
+ import { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'
83
86
  import { ChangeStageDialog } from './components/ChangeStageDialog'
84
87
  import { ChangeOwnerDialog } from './components/ChangeOwnerDialog'
85
88
  import { buildCrudExportUrl, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
@@ -410,6 +413,12 @@ function sortDeals(deals: DealCardData[], option: SortOption): DealCardData[] {
410
413
 
411
414
  export default function DealsKanbanPage(): React.ReactElement {
412
415
  const t = useT()
416
+ // Resolved through the component registry so downstream apps can replace,
417
+ // wrap, or props-transform the quick-add dialog without forking this page.
418
+ const QuickDealDialog = useRegisteredComponent<QuickDealDialogProps>(
419
+ QUICK_DEAL_DIALOG_COMPONENT_ID,
420
+ DefaultQuickDealDialog,
421
+ )
413
422
  const router = useRouter()
414
423
  const scopeVersion = useOrganizationScopeVersion()
415
424
  const queryClient = useQueryClient()
@@ -12,6 +12,7 @@ import { E as CoreEntities } from '#generated/entities.ids.generated'
12
12
  import { createProgressBar } from '@open-mercato/shared/lib/cli/progress'
13
13
  import { buildIndexDocument, type IndexCustomFieldValue } from '@open-mercato/core/modules/query_index/lib/document'
14
14
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
15
+ import { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'
15
16
  import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
16
17
  import type { EntityId } from '@open-mercato/shared/modules/entities'
17
18
  import {
@@ -1994,6 +1995,7 @@ async function seedCustomerStressTest(
1994
1995
  updated_at: Date
1995
1996
  deleted_at: null
1996
1997
  }> = []
1998
+ const searchConfig = resolveSearchConfig()
1997
1999
  for (const [entityType, bucket] of pendingIndexDocs.entries()) {
1998
2000
  for (const entry of bucket.values()) {
1999
2001
  if (!entry.baseRow || Object.keys(entry.baseRow).length === 0) continue
@@ -2002,10 +2004,15 @@ async function seedCustomerStressTest(
2002
2004
  entity_id: entry.recordId,
2003
2005
  organization_id: entry.organizationId,
2004
2006
  tenant_id: entry.tenantId,
2005
- doc: buildIndexDocument(entry.baseRow, entry.customFields, {
2006
- organizationId: entry.organizationId,
2007
- tenantId: entry.tenantId,
2008
- }),
2007
+ doc: buildIndexDocument(
2008
+ entry.baseRow,
2009
+ entry.customFields,
2010
+ {
2011
+ organizationId: entry.organizationId,
2012
+ tenantId: entry.tenantId,
2013
+ },
2014
+ { entityType, config: searchConfig },
2015
+ ),
2009
2016
  index_version: 1,
2010
2017
  created_at: entry.createdAt,
2011
2018
  updated_at: entry.updatedAt,
@@ -99,6 +99,8 @@ interface ScheduleActivityDialogProps {
99
99
  onActivityCreated?: () => void
100
100
  /** When provided, dialog opens in edit mode with pre-filled data */
101
101
  editData?: ScheduleActivityEditData | null
102
+ /** Default country ISO2 code for phone number input when value is empty/unparsed */
103
+ defaultCountryIso2?: string
102
104
  }
103
105
 
104
106
  export function ScheduleActivityDialog({
@@ -111,6 +113,7 @@ export function ScheduleActivityDialog({
111
113
  entityType,
112
114
  onActivityCreated,
113
115
  editData,
116
+ defaultCountryIso2,
114
117
  }: ScheduleActivityDialogProps) {
115
118
  const t = useT()
116
119
  const state = useScheduleFormState({ open, editData: editData ?? null })
@@ -689,6 +692,7 @@ export function ScheduleActivityDialog({
689
692
  externalError={callPhoneError}
690
693
  invalidLabel={callPhoneInvalidMessage}
691
694
  minDigits={7}
695
+ defaultCountryIso2={defaultCountryIso2}
692
696
  />
693
697
  </div>
694
698
  ) : (
@@ -418,7 +418,7 @@ const companyDictionaryFieldDefinitions: DictionaryFieldDefinition[] = [
418
418
  },
419
419
  ]
420
420
 
421
- const createPrimaryPhoneField = (t: Translator): CrudField => ({
421
+ const createPrimaryPhoneField = (t: Translator, defaultCountryIso2?: string): CrudField => ({
422
422
  id: 'primaryPhone',
423
423
  label: t('customers.people.form.primaryPhone'),
424
424
  type: 'custom',
@@ -447,6 +447,7 @@ const createPrimaryPhoneField = (t: Translator): CrudField => ({
447
447
  invalidLabel={t('customers.people.form.primaryPhone.invalid', 'Enter a valid phone number with country code (e.g. +1 212 555 1234)')}
448
448
  minDigits={7}
449
449
  onDuplicateLookup={!disabled && !error ? duplicateLookup : undefined}
450
+ defaultCountryIso2={defaultCountryIso2}
450
451
  />
451
452
  )
452
453
  },
@@ -869,7 +870,8 @@ export const createDisplayNameSection = (t: Translator) =>
869
870
  )
870
871
  }
871
872
 
872
- export const createPersonFormFields = (t: Translator): CrudField[] => {
873
+ export const createPersonFormFields = (t: Translator, options?: { defaultCountryIso2?: string }): CrudField[] => {
874
+ const defaultCountryIso2 = options?.defaultCountryIso2
873
875
  const contactSection = createSectionHeadingField('__contactInformationSection', t('customers.people.form.sections.contactInformation'))
874
876
  const companySection = createSectionHeadingField('__companyInformationSection', t('customers.people.form.sections.companyInformation'))
875
877
  const dictionaryFields: CrudField[] = dictionaryFieldDefinitions.map((definition) => ({
@@ -930,7 +932,7 @@ export const createPersonFormFields = (t: Translator): CrudField[] => {
930
932
  },
931
933
  contactSection,
932
934
  createPrimaryEmailField(t),
933
- createPrimaryPhoneField(t),
935
+ createPrimaryPhoneField(t, defaultCountryIso2),
934
936
  companySection,
935
937
  {
936
938
  id: 'companyEntityId',
@@ -1225,7 +1227,8 @@ export const createCompanyFormSchema = () =>
1225
1227
  })
1226
1228
  .passthrough()
1227
1229
 
1228
- export const createCompanyFormFields = (t: Translator): CrudField[] => {
1230
+ export const createCompanyFormFields = (t: Translator, options?: { defaultCountryIso2?: string }): CrudField[] => {
1231
+ const defaultCountryIso2 = options?.defaultCountryIso2
1229
1232
  const dictionaryFields: CrudField[] = companyDictionaryFieldDefinitions.map((definition) => ({
1230
1233
  id: definition.id,
1231
1234
  label: t(definition.labelKey),
@@ -1270,6 +1273,7 @@ export const createCompanyFormFields = (t: Translator): CrudField[] => {
1270
1273
  placeholder={t('customers.companies.form.primaryPhonePlaceholder', '+1 555 123 4567')}
1271
1274
  invalidLabel={t('customers.people.form.primaryPhone.invalid', 'Enter a valid phone number with country code (e.g. +1 212 555 1234)')}
1272
1275
  minDigits={7}
1276
+ defaultCountryIso2={defaultCountryIso2}
1273
1277
  />
1274
1278
  ),
1275
1279
  } as CrudField,
@@ -1651,8 +1655,8 @@ const buildIndustryLabels = (t: Translator): DictionarySelectLabels => ({
1651
1655
  manageTitle: t('customers.people.form.dictionary.manage'),
1652
1656
  })
1653
1657
 
1654
- export const createCompanyEditFields = (t: Translator): CrudField[] => {
1655
- const baseFields = createCompanyFormFields(t)
1658
+ export const createCompanyEditFields = (t: Translator, options?: { defaultCountryIso2?: string }): CrudField[] => {
1659
+ const baseFields = createCompanyFormFields(t, options)
1656
1660
  const industryLabels = buildIndustryLabels(t)
1657
1661
 
1658
1662
  return baseFields.map((field) => {
@@ -1676,8 +1680,8 @@ export const createCompanyEditFields = (t: Translator): CrudField[] => {
1676
1680
  })
1677
1681
  }
1678
1682
 
1679
- export const createPersonEditFields = (t: Translator): CrudField[] => {
1680
- const baseFields = createPersonFormFields(t)
1683
+ export const createPersonEditFields = (t: Translator, options?: { defaultCountryIso2?: string }): CrudField[] => {
1684
+ const baseFields = createPersonFormFields(t, options)
1681
1685
  return [
1682
1686
  ...baseFields,
1683
1687
  {
@@ -9,6 +9,8 @@ import {
9
9
  createWidgetDataService,
10
10
  type WidgetDataRequest,
11
11
  WidgetDataValidationError,
12
+ WidgetDataScanLimitError,
13
+ WidgetDataEncryptionUnavailableError,
12
14
  } from '../../../../services/widgetDataService'
13
15
  import { runWidgetDataBatch } from '../../../../lib/widgetDataBatch'
14
16
  import type { AnalyticsRegistry } from '../../../../services/analyticsRegistry'
@@ -125,6 +127,8 @@ export async function POST(req: Request) {
125
127
  fetchOne: (request) => service.fetchWidgetData(request),
126
128
  describeError: (error) =>
127
129
  error instanceof WidgetDataValidationError
130
+ || error instanceof WidgetDataScanLimitError
131
+ || error instanceof WidgetDataEncryptionUnavailableError
128
132
  ? error.message
129
133
  : 'An error occurred while processing your request',
130
134
  })
@@ -8,6 +8,8 @@ import {
8
8
  createWidgetDataService,
9
9
  type WidgetDataRequest,
10
10
  WidgetDataValidationError,
11
+ WidgetDataScanLimitError,
12
+ WidgetDataEncryptionUnavailableError,
11
13
  } from '../../../services/widgetDataService'
12
14
  import type { AnalyticsRegistry } from '../../../services/analyticsRegistry'
13
15
  import { runApiInterceptorsBefore } from '@open-mercato/shared/lib/crud/interceptor-runner'
@@ -129,6 +131,15 @@ export async function POST(req: Request) {
129
131
  if (err instanceof WidgetDataValidationError) {
130
132
  return NextResponse.json({ error: err.message }, { status: 400 })
131
133
  }
134
+ // The dataset is too large to group in application code; surface why rather than a masked 500.
135
+ if (err instanceof WidgetDataScanLimitError) {
136
+ return NextResponse.json({ error: err.message }, { status: 422 })
137
+ }
138
+ // Encryption is configured but currently unresolvable. Refusing the request keeps ciphertext and
139
+ // silently-wrong buckets out of the response, and 503 tells the client it is worth retrying.
140
+ if (err instanceof WidgetDataEncryptionUnavailableError) {
141
+ return NextResponse.json({ error: err.message }, { status: 503 })
142
+ }
132
143
  return NextResponse.json(
133
144
  { error: 'An error occurred while processing your request' },
134
145
  { status: 500 },
@@ -156,8 +167,18 @@ const widgetDataPostDoc: OpenApiMethodDoc = {
156
167
  errors: [
157
168
  { status: 400, description: 'Invalid request payload', schema: dashboardsErrorSchema },
158
169
  { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },
170
+ {
171
+ status: 422,
172
+ description: 'Too many rows to group an encrypted field in application code',
173
+ schema: dashboardsErrorSchema,
174
+ },
159
175
  { status: 403, description: 'Missing analytics.view feature', schema: dashboardsErrorSchema },
160
176
  { status: 500, description: 'Internal server error', schema: dashboardsErrorSchema },
177
+ {
178
+ status: 503,
179
+ description: 'Encryption is configured but the group source cannot currently be resolved',
180
+ schema: dashboardsErrorSchema,
181
+ },
161
182
  ],
162
183
  }
163
184