@open-mercato/core 0.6.1-develop.3276.1.1fcd145078 → 0.6.1-develop.3278.1.a4d26475e4

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 (212) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/domain_mapping/index.js +31 -0
  3. package/dist/generated/entities/domain_mapping/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +2 -1
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +16 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/helpers/integration/salesUi.js +18 -17
  9. package/dist/helpers/integration/salesUi.js.map +2 -2
  10. package/dist/modules/customer_accounts/acl.js +2 -1
  11. package/dist/modules/customer_accounts/acl.js.map +2 -2
  12. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js +110 -0
  13. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js.map +7 -0
  14. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js +114 -0
  15. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js.map +7 -0
  16. package/dist/modules/customer_accounts/api/admin/domain-mappings.js +271 -0
  17. package/dist/modules/customer_accounts/api/admin/domain-mappings.js.map +7 -0
  18. package/dist/modules/customer_accounts/api/domain-check.js +70 -0
  19. package/dist/modules/customer_accounts/api/domain-check.js.map +7 -0
  20. package/dist/modules/customer_accounts/api/domain-resolve/all.js +128 -0
  21. package/dist/modules/customer_accounts/api/domain-resolve/all.js.map +7 -0
  22. package/dist/modules/customer_accounts/api/domain-resolve.js +84 -0
  23. package/dist/modules/customer_accounts/api/domain-resolve.js.map +7 -0
  24. package/dist/modules/customer_accounts/api/login.js +14 -3
  25. package/dist/modules/customer_accounts/api/login.js.map +2 -2
  26. package/dist/modules/customer_accounts/api/magic-link/request.js +14 -3
  27. package/dist/modules/customer_accounts/api/magic-link/request.js.map +2 -2
  28. package/dist/modules/customer_accounts/api/password/reset-request.js +14 -3
  29. package/dist/modules/customer_accounts/api/password/reset-request.js.map +2 -2
  30. package/dist/modules/customer_accounts/api/signup.js +32 -5
  31. package/dist/modules/customer_accounts/api/signup.js.map +2 -2
  32. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js +48 -0
  33. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js.map +7 -0
  34. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +43 -0
  35. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +7 -0
  36. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js +53 -0
  37. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js.map +7 -0
  38. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js +69 -0
  39. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js.map +7 -0
  40. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js +31 -0
  41. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js.map +7 -0
  42. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js +147 -0
  43. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js.map +7 -0
  44. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js +39 -0
  45. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js.map +7 -0
  46. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js +60 -0
  47. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js.map +7 -0
  48. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js +11 -0
  49. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js.map +7 -0
  50. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js +1 -0
  51. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js.map +7 -0
  52. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js +361 -0
  53. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js.map +7 -0
  54. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js +28 -0
  55. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js.map +7 -0
  56. package/dist/modules/customer_accounts/data/enrichers.js +78 -12
  57. package/dist/modules/customer_accounts/data/enrichers.js.map +2 -2
  58. package/dist/modules/customer_accounts/data/entities.js +85 -2
  59. package/dist/modules/customer_accounts/data/entities.js.map +2 -2
  60. package/dist/modules/customer_accounts/data/guards.js +130 -0
  61. package/dist/modules/customer_accounts/data/guards.js.map +7 -0
  62. package/dist/modules/customer_accounts/data/validators.js +19 -0
  63. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  64. package/dist/modules/customer_accounts/di.js +23 -1
  65. package/dist/modules/customer_accounts/di.js.map +2 -2
  66. package/dist/modules/customer_accounts/events.js +9 -2
  67. package/dist/modules/customer_accounts/events.js.map +2 -2
  68. package/dist/modules/customer_accounts/lib/customerAuthServer.js +27 -7
  69. package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
  70. package/dist/modules/customer_accounts/lib/customerUrl.js +48 -0
  71. package/dist/modules/customer_accounts/lib/customerUrl.js.map +7 -0
  72. package/dist/modules/customer_accounts/lib/hostname.js +55 -0
  73. package/dist/modules/customer_accounts/lib/hostname.js.map +7 -0
  74. package/dist/modules/customer_accounts/lib/platformDomains.js +7 -0
  75. package/dist/modules/customer_accounts/lib/platformDomains.js.map +7 -0
  76. package/dist/modules/customer_accounts/lib/proxyRanges.js +71 -0
  77. package/dist/modules/customer_accounts/lib/proxyRanges.js.map +7 -0
  78. package/dist/modules/customer_accounts/lib/rateLimiter.js +7 -0
  79. package/dist/modules/customer_accounts/lib/rateLimiter.js.map +2 -2
  80. package/dist/modules/customer_accounts/lib/resolveTenantContext.js +63 -0
  81. package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +7 -0
  82. package/dist/modules/customer_accounts/lib/secretCompare.js +13 -0
  83. package/dist/modules/customer_accounts/lib/secretCompare.js.map +7 -0
  84. package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js +37 -0
  85. package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js.map +7 -0
  86. package/dist/modules/customer_accounts/notifications.client.js +76 -0
  87. package/dist/modules/customer_accounts/notifications.client.js.map +2 -2
  88. package/dist/modules/customer_accounts/notifications.js +76 -0
  89. package/dist/modules/customer_accounts/notifications.js.map +2 -2
  90. package/dist/modules/customer_accounts/services/domainMappingService.js +497 -0
  91. package/dist/modules/customer_accounts/services/domainMappingService.js.map +7 -0
  92. package/dist/modules/customer_accounts/setup.js +48 -1
  93. package/dist/modules/customer_accounts/setup.js.map +2 -2
  94. package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js +29 -0
  95. package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js.map +7 -0
  96. package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js +29 -0
  97. package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js.map +7 -0
  98. package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js +30 -0
  99. package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js.map +7 -0
  100. package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js +31 -0
  101. package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js.map +7 -0
  102. package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js +29 -0
  103. package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js.map +7 -0
  104. package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js +67 -0
  105. package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js.map +7 -0
  106. package/dist/modules/customer_accounts/workers/domainVerificationWorker.js +34 -0
  107. package/dist/modules/customer_accounts/workers/domainVerificationWorker.js.map +7 -0
  108. package/dist/modules/customers/api/companies/route.js +4 -9
  109. package/dist/modules/customers/api/companies/route.js.map +2 -2
  110. package/dist/modules/customers/api/deals/route.js +4 -9
  111. package/dist/modules/customers/api/deals/route.js.map +2 -2
  112. package/dist/modules/customers/api/people/route.js +4 -9
  113. package/dist/modules/customers/api/people/route.js.map +2 -2
  114. package/dist/modules/customers/api/utils.js +0 -12
  115. package/dist/modules/customers/api/utils.js.map +2 -2
  116. package/dist/modules/customers/backend/customers/companies/page.js +361 -284
  117. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  118. package/dist/modules/customers/backend/customers/deals/page.js +378 -473
  119. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  120. package/dist/modules/customers/backend/customers/people/page.js +368 -286
  121. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  122. package/dist/modules/customers/components/detail/assignableStaff.js +15 -1
  123. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  124. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +188 -36
  125. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +2 -2
  126. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +2 -2
  127. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +2 -2
  128. package/dist/modules/customers/widgets/injection-table.js +6 -0
  129. package/dist/modules/customers/widgets/injection-table.js.map +2 -2
  130. package/dist/modules/perspectives/services/perspectiveService.js +16 -2
  131. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  132. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +1 -0
  133. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
  134. package/dist/modules/query_index/lib/engine.js +31 -8
  135. package/dist/modules/query_index/lib/engine.js.map +2 -2
  136. package/generated/entities/domain_mapping/index.ts +14 -0
  137. package/generated/entities.ids.generated.ts +2 -1
  138. package/generated/entity-fields-registry.ts +16 -0
  139. package/package.json +5 -5
  140. package/src/helpers/integration/salesUi.ts +27 -23
  141. package/src/modules/customer_accounts/acl.ts +1 -0
  142. package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.ts +119 -0
  143. package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.ts +123 -0
  144. package/src/modules/customer_accounts/api/admin/domain-mappings.ts +303 -0
  145. package/src/modules/customer_accounts/api/domain-check.ts +83 -0
  146. package/src/modules/customer_accounts/api/domain-resolve/all.ts +139 -0
  147. package/src/modules/customer_accounts/api/domain-resolve.ts +91 -0
  148. package/src/modules/customer_accounts/api/login.ts +14 -3
  149. package/src/modules/customer_accounts/api/magic-link/request.ts +14 -3
  150. package/src/modules/customer_accounts/api/password/reset-request.ts +15 -3
  151. package/src/modules/customer_accounts/api/signup.ts +36 -5
  152. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.tsx +70 -0
  153. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +66 -0
  154. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.tsx +78 -0
  155. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.tsx +90 -0
  156. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.tsx +43 -0
  157. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.tsx +178 -0
  158. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.tsx +56 -0
  159. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.tsx +69 -0
  160. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.ts +10 -0
  161. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.ts +31 -0
  162. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.ts +26 -0
  163. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.tsx +411 -0
  164. package/src/modules/customer_accounts/data/enrichers.ts +95 -12
  165. package/src/modules/customer_accounts/data/entities.ts +80 -1
  166. package/src/modules/customer_accounts/data/guards.ts +155 -0
  167. package/src/modules/customer_accounts/data/validators.ts +26 -0
  168. package/src/modules/customer_accounts/di.ts +35 -1
  169. package/src/modules/customer_accounts/events.ts +8 -1
  170. package/src/modules/customer_accounts/i18n/de.json +78 -0
  171. package/src/modules/customer_accounts/i18n/en.json +78 -0
  172. package/src/modules/customer_accounts/i18n/es.json +78 -0
  173. package/src/modules/customer_accounts/i18n/pl.json +78 -0
  174. package/src/modules/customer_accounts/lib/customerAuthServer.ts +44 -8
  175. package/src/modules/customer_accounts/lib/customerUrl.ts +72 -0
  176. package/src/modules/customer_accounts/lib/hostname.ts +76 -0
  177. package/src/modules/customer_accounts/lib/platformDomains.ts +14 -0
  178. package/src/modules/customer_accounts/lib/proxyRanges.ts +85 -0
  179. package/src/modules/customer_accounts/lib/rateLimiter.ts +10 -0
  180. package/src/modules/customer_accounts/lib/resolveTenantContext.ts +96 -0
  181. package/src/modules/customer_accounts/lib/secretCompare.ts +20 -0
  182. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +341 -0
  183. package/src/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.ts +38 -0
  184. package/src/modules/customer_accounts/notifications.client.ts +76 -0
  185. package/src/modules/customer_accounts/notifications.ts +76 -0
  186. package/src/modules/customer_accounts/services/domainMappingService.ts +632 -0
  187. package/src/modules/customer_accounts/setup.ts +81 -1
  188. package/src/modules/customer_accounts/subscribers/invalidateDomainCache.ts +35 -0
  189. package/src/modules/customer_accounts/subscribers/notifyDomainActivated.ts +32 -0
  190. package/src/modules/customer_accounts/subscribers/notifyDomainDnsFailed.ts +33 -0
  191. package/src/modules/customer_accounts/subscribers/notifyDomainTlsFailed.ts +34 -0
  192. package/src/modules/customer_accounts/subscribers/notifyDomainVerified.ts +32 -0
  193. package/src/modules/customer_accounts/workers/domainTlsRetryWorker.ts +84 -0
  194. package/src/modules/customer_accounts/workers/domainVerificationWorker.ts +45 -0
  195. package/src/modules/customers/api/companies/route.ts +4 -9
  196. package/src/modules/customers/api/deals/route.ts +4 -9
  197. package/src/modules/customers/api/people/route.ts +4 -9
  198. package/src/modules/customers/api/utils.ts +0 -14
  199. package/src/modules/customers/backend/customers/companies/page.tsx +321 -237
  200. package/src/modules/customers/backend/customers/deals/page.tsx +321 -455
  201. package/src/modules/customers/backend/customers/people/page.tsx +331 -240
  202. package/src/modules/customers/components/detail/assignableStaff.ts +21 -0
  203. package/src/modules/customers/i18n/de.json +31 -0
  204. package/src/modules/customers/i18n/en.json +31 -0
  205. package/src/modules/customers/i18n/es.json +31 -0
  206. package/src/modules/customers/i18n/pl.json +31 -0
  207. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +197 -40
  208. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +13 -7
  209. package/src/modules/customers/widgets/injection-table.ts +6 -0
  210. package/src/modules/perspectives/services/perspectiveService.ts +30 -2
  211. package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +1 -0
  212. package/src/modules/query_index/lib/engine.ts +46 -8
@@ -37,7 +37,10 @@ import { cn } from '@open-mercato/shared/lib/utils'
37
37
 
38
38
  export const CUSTOMERS_AI_INJECT_AGENT_ID = 'customers.account_assistant'
39
39
 
40
- export type CustomersAiInjectView = 'customers.people.list' | 'customers.companies.list'
40
+ export type CustomersAiInjectView =
41
+ | 'customers.people.list'
42
+ | 'customers.companies.list'
43
+ | 'customers.deals.list'
41
44
 
42
45
  export interface CustomersAiInjectPageContext {
43
46
  view: CustomersAiInjectView
@@ -83,8 +86,9 @@ function readNumber(value: unknown): number {
83
86
  }
84
87
 
85
88
  function resolveView(tableId: string | null | undefined): CustomersAiInjectView {
86
- if (typeof tableId === 'string' && tableId.includes('companies')) {
87
- return 'customers.companies.list'
89
+ if (typeof tableId === 'string') {
90
+ if (tableId.includes('deals')) return 'customers.deals.list'
91
+ if (tableId.includes('companies')) return 'customers.companies.list'
88
92
  }
89
93
  return 'customers.people.list'
90
94
  }
@@ -115,6 +119,78 @@ function useCustomerSuggestions(
115
119
  ): AiChatSuggestion[] {
116
120
  const t = useT()
117
121
  return React.useMemo(() => {
122
+ if (view === 'customers.deals.list') {
123
+ if (hasSelection) {
124
+ return [
125
+ {
126
+ label: t(
127
+ 'customers.ai_assistant.suggestions.summarizeSelectedDeals',
128
+ 'Summarize selected deals',
129
+ ),
130
+ prompt: `Give me a summary of my ${selectedCount} selected deals — stage, value, and close date`,
131
+ icon: <Handshake className="size-4" />,
132
+ },
133
+ {
134
+ label: t(
135
+ 'customers.ai_assistant.suggestions.atRiskSelectedDeals',
136
+ 'Selected deals at risk',
137
+ ),
138
+ prompt: `Of my ${selectedCount} selected deals, which look at risk of slipping their close date?`,
139
+ icon: <Handshake className="size-4" />,
140
+ },
141
+ {
142
+ label: t(
143
+ 'customers.ai_assistant.suggestions.companiesForSelectedDeals',
144
+ 'Companies on selected deals',
145
+ ),
146
+ prompt: `List the companies associated with my ${selectedCount} selected deals`,
147
+ icon: <Building2 className="size-4" />,
148
+ },
149
+ ]
150
+ }
151
+ return [
152
+ {
153
+ label: t(
154
+ 'customers.ai_assistant.suggestions.dealsClosingThisMonth',
155
+ 'Deals closing this month',
156
+ ),
157
+ prompt: 'Which deals are closing this month?',
158
+ icon: <Handshake className="size-4" />,
159
+ },
160
+ {
161
+ label: t(
162
+ 'customers.ai_assistant.suggestions.dealsAtRisk',
163
+ 'Deals at risk of slipping',
164
+ ),
165
+ prompt: 'Show me deals at risk of slipping their close date',
166
+ icon: <Handshake className="size-4" />,
167
+ },
168
+ {
169
+ label: t(
170
+ 'customers.ai_assistant.suggestions.totalOpenDealValue',
171
+ 'Total open deal value',
172
+ ),
173
+ prompt: "What's the total value of open deals across the pipeline?",
174
+ icon: <Handshake className="size-4" />,
175
+ },
176
+ {
177
+ label: t(
178
+ 'customers.ai_assistant.suggestions.topDealsByValue',
179
+ 'Top deals by value',
180
+ ),
181
+ prompt: 'Show me the top 5 deals by value',
182
+ icon: <Handshake className="size-4" />,
183
+ },
184
+ {
185
+ label: t(
186
+ 'customers.ai_assistant.suggestions.searchDeals',
187
+ 'Search for a deal',
188
+ ),
189
+ prompt: 'Search for deals by title, company, or stage',
190
+ icon: <Search className="size-4" />,
191
+ },
192
+ ]
193
+ }
118
194
  if (view === 'customers.companies.list') {
119
195
  if (hasSelection) {
120
196
  return [
@@ -228,24 +304,115 @@ function useCustomerContextItems(pageContext: CustomersAiInjectPageContext): AiC
228
304
  return React.useMemo(() => {
229
305
  const items: AiChatContextItem[] = []
230
306
  const { selectedCount, totalMatching } = pageContext.extra
231
- const isCompanies = pageContext.view === 'customers.companies.list'
307
+ const view = pageContext.view
308
+ let selectedKey = 'customers.ai_assistant.context.selectedPeople'
309
+ let selectedFallback = '{count} contacts selected'
310
+ let matchingKey = 'customers.ai_assistant.context.matchingPeople'
311
+ let matchingFallback = '{count} contacts in view'
312
+ if (view === 'customers.companies.list') {
313
+ selectedKey = 'customers.ai_assistant.context.selectedCompanies'
314
+ selectedFallback = '{count} companies selected'
315
+ matchingKey = 'customers.ai_assistant.context.matchingCompanies'
316
+ matchingFallback = '{count} companies in view'
317
+ } else if (view === 'customers.deals.list') {
318
+ selectedKey = 'customers.ai_assistant.context.selectedDeals'
319
+ selectedFallback = '{count} deals selected'
320
+ matchingKey = 'customers.ai_assistant.context.matchingDeals'
321
+ matchingFallback = '{count} deals in view'
322
+ }
232
323
  if (selectedCount > 0) {
233
- const key = isCompanies
234
- ? 'customers.ai_assistant.context.selectedCompanies'
235
- : 'customers.ai_assistant.context.selectedPeople'
236
- const fallback = isCompanies ? '{count} companies selected' : '{count} contacts selected'
237
- items.push({ label: t(key, fallback).replace('{count}', String(selectedCount)) })
324
+ items.push({ label: t(selectedKey, selectedFallback).replace('{count}', String(selectedCount)) })
238
325
  } else if (totalMatching > 0) {
239
- const key = isCompanies
240
- ? 'customers.ai_assistant.context.matchingCompanies'
241
- : 'customers.ai_assistant.context.matchingPeople'
242
- const fallback = isCompanies ? '{count} companies in view' : '{count} contacts in view'
243
- items.push({ label: t(key, fallback).replace('{count}', String(totalMatching)) })
326
+ items.push({ label: t(matchingKey, matchingFallback).replace('{count}', String(totalMatching)) })
244
327
  }
245
328
  return items
246
329
  }, [pageContext, t])
247
330
  }
248
331
 
332
+ interface ViewCopy {
333
+ welcomeDescriptionAll: string
334
+ welcomeDescriptionSelection: string
335
+ descriptionWithSelection: string
336
+ triggerAriaLabel: string
337
+ }
338
+
339
+ function useViewCopy(view: CustomersAiInjectView, selectedCount: number): ViewCopy {
340
+ const t = useT()
341
+ return React.useMemo(() => {
342
+ const replaceCount = (s: string) => s.replace('{count}', String(selectedCount))
343
+ if (view === 'customers.deals.list') {
344
+ return {
345
+ welcomeDescriptionAll: t(
346
+ 'customers.ai_assistant.sheet.welcomeDescriptionAllDeals',
347
+ 'Ask about deals, pipeline, or close dates:',
348
+ ),
349
+ welcomeDescriptionSelection: replaceCount(
350
+ t(
351
+ 'customers.ai_assistant.sheet.welcomeDescriptionSelectionDeals',
352
+ 'Ready to explore your {count} selected deals:',
353
+ ),
354
+ ),
355
+ descriptionWithSelection: replaceCount(
356
+ t(
357
+ 'customers.ai_assistant.sheet.descriptionWithSelectionDeals',
358
+ 'Working with {count} selected deals. Ask about their stage, value, and close dates.',
359
+ ),
360
+ ),
361
+ triggerAriaLabel: t(
362
+ 'customers.ai_assistant.trigger.ariaLabelDeals',
363
+ 'Open AI assistant for deals',
364
+ ),
365
+ }
366
+ }
367
+ if (view === 'customers.companies.list') {
368
+ return {
369
+ welcomeDescriptionAll: t(
370
+ 'customers.ai_assistant.sheet.welcomeDescriptionAllCompanies',
371
+ 'Ask me anything about your companies, deals, and activities:',
372
+ ),
373
+ welcomeDescriptionSelection: replaceCount(
374
+ t(
375
+ 'customers.ai_assistant.sheet.welcomeDescriptionSelectionCompanies',
376
+ 'Ready to explore your {count} selected companies:',
377
+ ),
378
+ ),
379
+ descriptionWithSelection: replaceCount(
380
+ t(
381
+ 'customers.ai_assistant.sheet.descriptionWithSelectionCompanies',
382
+ 'Working with {count} selected companies. Ask about their deals, contacts, and activities.',
383
+ ),
384
+ ),
385
+ triggerAriaLabel: t(
386
+ 'customers.ai_assistant.trigger.ariaLabelCompanies',
387
+ 'Open AI assistant for companies',
388
+ ),
389
+ }
390
+ }
391
+ return {
392
+ welcomeDescriptionAll: t(
393
+ 'customers.ai_assistant.sheet.welcomeDescriptionAll',
394
+ 'Ask me anything about your customers, companies, and deals:',
395
+ ),
396
+ welcomeDescriptionSelection: replaceCount(
397
+ t(
398
+ 'customers.ai_assistant.sheet.welcomeDescriptionSelection',
399
+ 'Ready to explore your {count} selected contacts:',
400
+ ),
401
+ ),
402
+ descriptionWithSelection: replaceCount(
403
+ t(
404
+ 'customers.ai_assistant.sheet.descriptionWithSelection',
405
+ 'Working with {count} selected contacts. Ask about their details, deals, companies, and activities.',
406
+ ),
407
+ ),
408
+ triggerAriaLabel: t(
409
+ 'customers.ai_assistant.trigger.ariaLabel',
410
+ 'Open AI assistant for people',
411
+ ),
412
+ }
413
+ }, [view, selectedCount, t])
414
+ }
415
+
249
416
  interface CustomerAgentDescriptor {
250
417
  id: string
251
418
  label: string
@@ -285,6 +452,7 @@ export default function AiAssistantTriggerWidget({ context }: AiAssistantTrigger
285
452
  const hasSelection = selectedCount > 0
286
453
  const suggestions = useCustomerSuggestions(pageContext.view, hasSelection, selectedCount)
287
454
  const contextItems = useCustomerContextItems(pageContext)
455
+ const viewCopy = useViewCopy(pageContext.view, selectedCount)
288
456
 
289
457
  const openAgent = React.useCallback((agentId: string) => {
290
458
  setActiveAgent(agentId)
@@ -330,14 +498,8 @@ export default function AiAssistantTriggerWidget({ context }: AiAssistantTrigger
330
498
  contextItems,
331
499
  welcomeTitle: t('customers.ai_assistant.sheet.welcomeTitle', 'CRM Assistant'),
332
500
  welcomeDescription: hasSelection
333
- ? t(
334
- 'customers.ai_assistant.sheet.welcomeDescriptionSelection',
335
- 'Ready to explore your {count} selected contacts:',
336
- ).replace('{count}', String(selectedCount))
337
- : t(
338
- 'customers.ai_assistant.sheet.welcomeDescriptionAll',
339
- 'Ask me anything about your customers, companies, and deals:',
340
- ),
501
+ ? viewCopy.welcomeDescriptionSelection
502
+ : viewCopy.welcomeDescriptionAll,
341
503
  })
342
504
  setOpen(false)
343
505
  }, [
@@ -347,15 +509,12 @@ export default function AiAssistantTriggerWidget({ context }: AiAssistantTrigger
347
509
  dock,
348
510
  hasSelection,
349
511
  pageContext,
350
- selectedCount,
351
512
  suggestions,
352
513
  t,
514
+ viewCopy,
353
515
  ])
354
516
 
355
- const triggerLabel = t(
356
- 'customers.ai_assistant.trigger.ariaLabel',
357
- 'Open AI assistant for people',
358
- )
517
+ const triggerLabel = viewCopy.triggerAriaLabel
359
518
 
360
519
  const labelText = t('customers.ai_assistant.trigger.label', 'AI')
361
520
  const moreAgentsLabel = t(
@@ -375,10 +534,14 @@ export default function AiAssistantTriggerWidget({ context }: AiAssistantTrigger
375
534
  title={triggerLabel}
376
535
  className={cn(
377
536
  'relative',
537
+ 'hover:bg-brand-violet/10',
378
538
  agents.length > 1 && 'rounded-r-none border-r-0',
379
539
  )}
380
540
  >
381
- <Sparkles className="size-4" aria-hidden />
541
+ <Sparkles
542
+ className="size-4 text-brand-violet"
543
+ aria-hidden
544
+ />
382
545
  <span>{labelText}</span>
383
546
  {hasSelection ? (
384
547
  <span
@@ -487,10 +650,7 @@ export default function AiAssistantTriggerWidget({ context }: AiAssistantTrigger
487
650
  </div>
488
651
  <DialogDescription>
489
652
  {hasSelection
490
- ? t(
491
- 'customers.ai_assistant.sheet.descriptionWithSelection',
492
- 'Working with {count} selected contacts. Ask about their details, deals, companies, and activities.',
493
- ).replace('{count}', String(selectedCount))
653
+ ? viewCopy.descriptionWithSelection
494
654
  : t(
495
655
  'customers.ai_assistant.sheet.description',
496
656
  'Your CRM assistant. Ask about people, companies, deals, and activities.',
@@ -504,6 +664,7 @@ export default function AiAssistantTriggerWidget({ context }: AiAssistantTrigger
504
664
  contextItems={contextItems}
505
665
  hasSelection={hasSelection}
506
666
  selectedCount={selectedCount}
667
+ viewCopy={viewCopy}
507
668
  />
508
669
  </DialogContent>
509
670
  </Dialog>
@@ -518,6 +679,7 @@ interface CustomersChatBodyProps {
518
679
  contextItems: AiChatContextItem[]
519
680
  hasSelection: boolean
520
681
  selectedCount: number
682
+ viewCopy: ViewCopy
521
683
  }
522
684
 
523
685
  function CustomersChatBody({
@@ -527,6 +689,7 @@ function CustomersChatBody({
527
689
  contextItems,
528
690
  hasSelection,
529
691
  selectedCount,
692
+ viewCopy,
530
693
  }: CustomersChatBodyProps) {
531
694
  const t = useT()
532
695
  const sessions = useAiChatSessions()
@@ -563,14 +726,8 @@ function CustomersChatBody({
563
726
  welcomeTitle={t('customers.ai_assistant.sheet.welcomeTitle', 'CRM Assistant')}
564
727
  welcomeDescription={
565
728
  hasSelection
566
- ? t(
567
- 'customers.ai_assistant.sheet.welcomeDescriptionSelection',
568
- 'Ready to explore your {count} selected contacts:',
569
- ).replace('{count}', String(selectedCount))
570
- : t(
571
- 'customers.ai_assistant.sheet.welcomeDescriptionAll',
572
- 'Ask me anything about your customers, companies, and deals:',
573
- )
729
+ ? viewCopy.welcomeDescriptionSelection
730
+ : viewCopy.welcomeDescriptionAll
574
731
  }
575
732
  />
576
733
  ) : null}
@@ -5,28 +5,34 @@ import AiAssistantTriggerWidget from './widget.client'
5
5
  * Step 4.10 — Backend AiChat injection example.
6
6
  *
7
7
  * Demonstrates how a third-party module can drop `<AiChat>` onto a page
8
- * it does NOT own (the customers People list) via the existing widget
9
- * injection system. Targets spot `data-table:customers.people.list:header`,
10
- * which is owned by the `DataTable` primitive in `packages/ui`.
8
+ * it does NOT own (the customers People / Companies / Deals lists) via
9
+ * the existing widget injection system. Targets DataTable header /
10
+ * search-trailing spots owned by the `DataTable` primitive in
11
+ * `packages/ui`.
11
12
  *
12
13
  * The trigger button opens a right-side sheet embedding
13
14
  * `<AiChat agent="customers.account_assistant" pageContext={...} />`.
14
15
  * `pageContext` follows the spec §10.1 shape:
15
16
  *
16
- * { view: 'customers.people.list',
17
+ * { view: 'customers.people.list' | 'customers.companies.list' | 'customers.deals.list',
17
18
  * recordType: null,
18
19
  * recordId: string, // "" or comma-separated UUIDs
19
20
  * extra: { selectedCount, totalMatching } }
20
21
  *
21
- * Feature-gated behind `customers.people.view` + `ai_assistant.view`.
22
+ * Feature-gated behind `ai_assistant.view` only — the host CRM page
23
+ * already enforces its own `customers.people.view`/`companies.view`/
24
+ * `deals.view` guard, so the widget gate just needs to ensure the user
25
+ * has AI assistant access. (Earlier versions also listed
26
+ * `customers.people.view`, which is AND-evaluated by the registry and
27
+ * hid the trigger from companies-only or deals-only viewers.)
22
28
  */
23
29
  const widget: InjectionWidgetModule<Record<string, unknown>, Record<string, unknown>> = {
24
30
  metadata: {
25
31
  id: 'customers.injection.ai-assistant-trigger',
26
32
  title: 'Customers AI Assistant Trigger',
27
33
  description:
28
- 'Renders an "Ask AI" button in the people list header that opens a sheet embedding the customers account assistant.',
29
- features: ['customers.people.view', 'ai_assistant.view'],
34
+ 'Renders an "Ask AI" button in the people / companies / deals list headers that opens a sheet embedding the customers account assistant.',
35
+ features: ['ai_assistant.view'],
30
36
  requiredModules: ['ai_assistant'],
31
37
  priority: 100,
32
38
  enabled: true,
@@ -30,6 +30,12 @@ export const injectionTable: ModuleInjectionTable = {
30
30
  priority: 100,
31
31
  },
32
32
  ],
33
+ 'data-table:customers.deals.list:search-trailing': [
34
+ {
35
+ widgetId: 'customers.injection.ai-assistant-trigger',
36
+ priority: 100,
37
+ },
38
+ ],
33
39
  'detail:customers.deal:header': [
34
40
  {
35
41
  widgetId: 'customers.injection.ai-deal-detail-trigger',
@@ -95,26 +95,54 @@ function isPerspectivesState(value: unknown): value is PerspectivesState {
95
95
  return true
96
96
  }
97
97
 
98
+ /**
99
+ * Defensive migration for legacy filter state shapes captured before the
100
+ * advanced-filter tree (SPEC-048). Existing perspectives only store either
101
+ * advanced-filter URL params (tree shape with `v:2` or a `root` key) or
102
+ * undefined — this helper is a safety net for legacy `FilterValues`-shaped
103
+ * records (flat key/value records of column filters) that could only appear
104
+ * if old saved-view JSON were imported.
105
+ *
106
+ * - Tree-shaped state (`v:2` or `root` key) is passed through unchanged.
107
+ * - Undefined / null filters are passed through unchanged.
108
+ * - Legacy `FilterValues`-shaped records are dropped (set to `undefined`)
109
+ * because there is no reliable mapping back to the new operator model;
110
+ * the user sees an empty tree and can recreate.
111
+ */
112
+ export function maybeMigrateLegacyFilterValues(settings: PerspectiveSettings): PerspectiveSettings {
113
+ const filters = settings.filters
114
+ if (!filters || typeof filters !== 'object') return settings
115
+ const record = filters as Record<string, unknown>
116
+ if ('v' in record && record.v === 2) return settings
117
+ if ('root' in record) return settings
118
+ if (typeof console !== 'undefined') {
119
+ console.warn('[perspectives] Dropping legacy filterValues shape; please re-create the perspective with the new filter UI.')
120
+ }
121
+ return { ...settings, filters: undefined }
122
+ }
123
+
98
124
  function toResolvedPerspective(entity: Perspective): ResolvedPerspective {
125
+ const settings = maybeMigrateLegacyFilterValues((entity.settingsJson ?? {}) as PerspectiveSettings)
99
126
  return {
100
127
  id: entity.id,
101
128
  name: entity.name,
102
129
  tableId: entity.tableId,
103
130
  isDefault: !!entity.isDefault,
104
- settings: (entity.settingsJson ?? {}) as PerspectiveSettings,
131
+ settings,
105
132
  createdAt: entity.createdAt.toISOString(),
106
133
  updatedAt: entity.updatedAt ? entity.updatedAt.toISOString() : null,
107
134
  }
108
135
  }
109
136
 
110
137
  function toResolvedRolePerspective(entity: RolePerspective): ResolvedRolePerspective {
138
+ const settings = maybeMigrateLegacyFilterValues((entity.settingsJson ?? {}) as PerspectiveSettings)
111
139
  return {
112
140
  id: entity.id,
113
141
  roleId: entity.roleId,
114
142
  tableId: entity.tableId,
115
143
  name: entity.name,
116
144
  isDefault: !!entity.isDefault,
117
- settings: (entity.settingsJson ?? {}) as PerspectiveSettings,
145
+ settings,
118
146
  tenantId: nullish(entity.tenantId),
119
147
  organizationId: nullish(entity.organizationId),
120
148
  createdAt: entity.createdAt.toISOString(),
@@ -39,6 +39,7 @@ export default function PortalLoginPage({ params }: Props) {
39
39
  const result = await apiCall<{ ok: boolean; error?: string }>('/api/customer_accounts/login', {
40
40
  method: 'POST',
41
41
  headers: { 'Content-Type': 'application/json' },
42
+ credentials: 'include',
42
43
  body: JSON.stringify({ email, password, tenantId: tenant.tenantId }),
43
44
  })
44
45
 
@@ -625,14 +625,19 @@ export class HybridQueryEngine implements QueryEngine {
625
625
  existing.push(filter)
626
626
  groups.set(filter.orGroup, existing)
627
627
  }
628
- let next = q
629
- for (const [, groupFilters] of groups) {
630
- if (!groupFilters.length) continue
631
- next = next.where((eb: any) => eb.or(
632
- groupFilters.map((filter) => this.buildBaseFilterExpression(eb, filter, resolveBaseColumn, qualify, entity, searchRuntime))
633
- ))
634
- }
635
- return next
628
+ const groupList = Array.from(groups.values()).filter((g) => g.length > 0)
629
+ if (groupList.length === 0) return q
630
+ // Combine all groups in a single WHERE: disjuncts are OR'd together; within
631
+ // each disjunct, fields are AND'd. Building this as separate `.where()` calls
632
+ // would AND the disjuncts (wrong semantics).
633
+ return q.where((eb: any) => eb.or(
634
+ groupList.map((groupFilters) => {
635
+ const parts = groupFilters.map((filter) =>
636
+ this.buildBaseFilterExpression(eb, filter, resolveBaseColumn, qualify, entity, searchRuntime),
637
+ )
638
+ return parts.length === 1 ? parts[0] : eb.and(parts)
639
+ }),
640
+ ))
636
641
  }
637
642
 
638
643
  const applyAliasScopes = async (target: AnyBuilder, aliasName: string): Promise<AnyBuilder> => {
@@ -1341,6 +1346,39 @@ export class HybridQueryEngine implements QueryEngine {
1341
1346
  // Doc-based filter via `ei` alias — returned as EXISTS where possible
1342
1347
  return this.buildIndexDocFilterExpression(eb, 'ei', entity, fieldName, filter.op, filter.value, 'b.id', searchRuntime)
1343
1348
  }
1349
+ // For like/ilike with active search-tokens, route through hashed-token EXISTS subquery
1350
+ // so encrypted-at-rest columns can still be searched.
1351
+ if (
1352
+ (filter.op === 'like' || filter.op === 'ilike') &&
1353
+ searchRuntime?.enabled &&
1354
+ typeof filter.value === 'string'
1355
+ ) {
1356
+ const tokens = tokenizeText(String(filter.value), searchRuntime.config)
1357
+ if (tokens.hashes.length) {
1358
+ const sources: SearchTokenSource[] = (searchRuntime.searchSources && searchRuntime.searchSources.length
1359
+ ? searchRuntime.searchSources
1360
+ : [{ entity: String(entity), recordIdColumn: 'b.id' }]
1361
+ ).filter((src) => src.recordIdColumn && src.entity)
1362
+ if (sources.length) {
1363
+ return eb.or(
1364
+ sources.map((src) =>
1365
+ eb.exists(this.buildSearchTokensSub(eb, {
1366
+ entity: src.entity,
1367
+ field: fieldName,
1368
+ hashes: tokens.hashes,
1369
+ recordIdColumn: src.recordIdColumn,
1370
+ tenantId: searchRuntime.tenantId ?? null,
1371
+ organizationScope: searchRuntime.organizationScope ?? null,
1372
+ })),
1373
+ ),
1374
+ )
1375
+ }
1376
+ }
1377
+ // Tokenizer produced no hashes (e.g. value too short). Match the regular-base-filter
1378
+ // path's behavior of skipping the predicate (no filter), which is preferable to
1379
+ // silently turning into a plain `ilike` against an encrypted column.
1380
+ return sql<boolean>`true`
1381
+ }
1344
1382
  return this.buildColumnFilterExpression(eb, qualify(baseField), filter.op, filter.value)
1345
1383
  }
1346
1384