@open-mercato/core 0.9.0 → 0.9.1-develop.7462.1.94c09f158f

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 (276) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_dictionary_entry/index.js +2 -0
  3. package/dist/generated/entities/customer_dictionary_entry/index.js.map +2 -2
  4. package/dist/generated/entities/customer_interaction/index.js +2 -0
  5. package/dist/generated/entities/customer_interaction/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/attachments/api/route.js +29 -3
  9. package/dist/modules/attachments/api/route.js.map +2 -2
  10. package/dist/modules/attachments/lib/ocrLimits.js +49 -0
  11. package/dist/modules/attachments/lib/ocrLimits.js.map +7 -0
  12. package/dist/modules/attachments/lib/ocrQueue.js +76 -2
  13. package/dist/modules/attachments/lib/ocrQueue.js.map +2 -2
  14. package/dist/modules/attachments/lib/ocrService.js +4 -0
  15. package/dist/modules/attachments/lib/ocrService.js.map +2 -2
  16. package/dist/modules/attachments/lib/pdfProcessing.js +14 -2
  17. package/dist/modules/attachments/lib/pdfProcessing.js.map +2 -2
  18. package/dist/modules/attachments/lib/scoped-upload-service.js +4 -2
  19. package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
  20. package/dist/modules/attachments/lib/textExtraction.js +13 -1
  21. package/dist/modules/attachments/lib/textExtraction.js.map +2 -2
  22. package/dist/modules/catalog/commands/categories.js +1 -1
  23. package/dist/modules/catalog/commands/categories.js.map +2 -2
  24. package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
  25. package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
  26. package/dist/modules/catalog/commands/products.js +52 -17
  27. package/dist/modules/catalog/commands/products.js.map +2 -2
  28. package/dist/modules/catalog/search.js +3 -1
  29. package/dist/modules/catalog/search.js.map +2 -2
  30. package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js +2 -1
  31. package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js.map +2 -2
  32. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
  33. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
  34. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
  35. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
  36. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
  37. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
  38. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +10 -7
  39. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +2 -2
  40. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
  41. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  42. package/dist/modules/communication_channels/lib/inbound-message-origin.js +1 -1
  43. package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
  44. package/dist/modules/communication_channels/lib/oauth-result.js +12 -0
  45. package/dist/modules/communication_channels/lib/oauth-result.js.map +7 -0
  46. package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
  47. package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
  48. package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
  49. package/dist/modules/communication_channels/lib/use-connect-channel.js.map +2 -2
  50. package/dist/modules/customers/api/activity-types/route.js +151 -0
  51. package/dist/modules/customers/api/activity-types/route.js.map +7 -0
  52. package/dist/modules/customers/api/deals/[id]/people/route.js +58 -5
  53. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  54. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
  55. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
  56. package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
  57. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  58. package/dist/modules/customers/api/interactions/route.js +3 -0
  59. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  60. package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
  61. package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
  62. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
  63. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
  64. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
  65. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
  66. package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
  67. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  68. package/dist/modules/customers/calendar-event-types.js +548 -0
  69. package/dist/modules/customers/calendar-event-types.js.map +7 -0
  70. package/dist/modules/customers/commands/dictionaries.js +70 -10
  71. package/dist/modules/customers/commands/dictionaries.js.map +2 -2
  72. package/dist/modules/customers/commands/interactions.js +187 -22
  73. package/dist/modules/customers/commands/interactions.js.map +3 -3
  74. package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
  75. package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
  76. package/dist/modules/customers/components/DictionarySettings.js +3 -1
  77. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  78. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
  79. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
  80. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
  81. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  82. package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
  83. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  84. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
  85. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
  86. package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
  87. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
  88. package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
  89. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  90. package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
  91. package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
  92. package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
  93. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
  94. package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
  95. package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
  96. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
  97. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  98. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
  99. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
  100. package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
  101. package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
  102. package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
  103. package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
  104. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
  105. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
  106. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
  107. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
  108. package/dist/modules/customers/components/calendar/types.js +1 -0
  109. package/dist/modules/customers/components/calendar/types.js.map +2 -2
  110. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +78 -343
  111. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  112. package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
  113. package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
  114. package/dist/modules/customers/components/detail/DealForm.js +7 -3
  115. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  116. package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
  117. package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
  118. package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
  119. package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
  120. package/dist/modules/customers/components/detail/PersonCard.js +5 -2
  121. package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
  122. package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
  123. package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
  124. package/dist/modules/customers/data/entities.js +6 -0
  125. package/dist/modules/customers/data/entities.js.map +2 -2
  126. package/dist/modules/customers/data/validators.js +39 -10
  127. package/dist/modules/customers/data/validators.js.map +2 -2
  128. package/dist/modules/customers/di.js +20 -1
  129. package/dist/modules/customers/di.js.map +2 -2
  130. package/dist/modules/customers/extension-points.js +13 -0
  131. package/dist/modules/customers/extension-points.js.map +2 -2
  132. package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
  133. package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
  134. package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
  135. package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
  136. package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
  137. package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
  138. package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
  139. package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
  140. package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
  141. package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
  142. package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
  143. package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
  144. package/dist/modules/customers/lib/calendar/timezone.js +111 -0
  145. package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
  146. package/dist/modules/customers/lib/interactionReadModel.js +1 -0
  147. package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
  148. package/dist/modules/customers/lib/todoCompatibility.js +1 -0
  149. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  150. package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
  151. package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
  152. package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
  153. package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
  154. package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
  155. package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
  156. package/dist/modules/messages/subscribers/message-notification.js +13 -1
  157. package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
  158. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
  159. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  160. package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
  161. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  162. package/dist/modules/planner/lib/availabilityMerge.js +101 -38
  163. package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
  164. package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
  165. package/dist/modules/sales/components/documents/LineItemDialog.js +159 -81
  166. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  167. package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
  168. package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
  169. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
  170. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  171. package/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
  172. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  173. package/generated/entities/customer_dictionary_entry/index.ts +1 -0
  174. package/generated/entities/customer_interaction/index.ts +1 -0
  175. package/generated/entity-fields-registry.ts +2 -0
  176. package/package.json +9 -8
  177. package/src/modules/attachments/api/route.ts +36 -3
  178. package/src/modules/attachments/lib/ocrLimits.ts +62 -0
  179. package/src/modules/attachments/lib/ocrQueue.ts +95 -2
  180. package/src/modules/attachments/lib/ocrService.ts +4 -0
  181. package/src/modules/attachments/lib/pdfProcessing.ts +14 -2
  182. package/src/modules/attachments/lib/scoped-upload-service.ts +4 -2
  183. package/src/modules/attachments/lib/textExtraction.ts +14 -1
  184. package/src/modules/auth/i18n/de.json +3 -0
  185. package/src/modules/auth/i18n/en.json +3 -0
  186. package/src/modules/auth/i18n/es.json +3 -0
  187. package/src/modules/auth/i18n/ko.json +3 -0
  188. package/src/modules/auth/i18n/pl.json +3 -0
  189. package/src/modules/catalog/commands/categories.ts +1 -1
  190. package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
  191. package/src/modules/catalog/commands/products.ts +58 -17
  192. package/src/modules/catalog/search.ts +5 -1
  193. package/src/modules/communication_channels/api/get/oauth/[provider]/callback/route.ts +2 -1
  194. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
  195. package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
  196. package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
  197. package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +12 -9
  198. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
  199. package/src/modules/communication_channels/i18n/de.json +4 -1
  200. package/src/modules/communication_channels/i18n/en.json +4 -1
  201. package/src/modules/communication_channels/i18n/es.json +4 -1
  202. package/src/modules/communication_channels/i18n/ko.json +4 -1
  203. package/src/modules/communication_channels/i18n/pl.json +4 -1
  204. package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
  205. package/src/modules/communication_channels/lib/oauth-result.ts +13 -0
  206. package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
  207. package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
  208. package/src/modules/customers/api/activity-types/route.ts +154 -0
  209. package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
  210. package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
  211. package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
  212. package/src/modules/customers/api/interactions/route.ts +4 -0
  213. package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
  214. package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
  215. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
  216. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
  217. package/src/modules/customers/calendar-event-types.ts +674 -0
  218. package/src/modules/customers/commands/dictionaries.ts +84 -8
  219. package/src/modules/customers/commands/interactions.ts +244 -21
  220. package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
  221. package/src/modules/customers/components/DictionarySettings.tsx +5 -3
  222. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
  223. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
  224. package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
  225. package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
  226. package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
  227. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
  228. package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
  229. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
  230. package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
  231. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
  232. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
  233. package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
  234. package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
  235. package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
  236. package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
  237. package/src/modules/customers/components/calendar/types.ts +1 -0
  238. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +94 -388
  239. package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
  240. package/src/modules/customers/components/detail/DealForm.tsx +7 -3
  241. package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
  242. package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
  243. package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
  244. package/src/modules/customers/components/detail/types.ts +1 -0
  245. package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -0
  246. package/src/modules/customers/data/entities.ts +8 -1
  247. package/src/modules/customers/data/validators.ts +42 -7
  248. package/src/modules/customers/di.ts +21 -0
  249. package/src/modules/customers/extension-points.ts +13 -0
  250. package/src/modules/customers/i18n/de.json +104 -8
  251. package/src/modules/customers/i18n/en.json +104 -8
  252. package/src/modules/customers/i18n/es.json +104 -8
  253. package/src/modules/customers/i18n/ko.json +104 -8
  254. package/src/modules/customers/i18n/pl.json +104 -8
  255. package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
  256. package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
  257. package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
  258. package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
  259. package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
  260. package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
  261. package/src/modules/customers/lib/calendar/timezone.ts +117 -0
  262. package/src/modules/customers/lib/interactionReadModel.ts +1 -0
  263. package/src/modules/customers/lib/todoCompatibility.ts +2 -0
  264. package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
  265. package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
  266. package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
  267. package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
  268. package/src/modules/messages/subscribers/message-notification.ts +13 -3
  269. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
  270. package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
  271. package/src/modules/planner/lib/availabilityMerge.ts +106 -41
  272. package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
  273. package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
  274. package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
  275. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
  276. package/src/modules/sync_excel/lib/adapters/customers.ts +33 -27
@@ -10,7 +10,12 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
10
10
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
11
11
  import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
12
12
  import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
13
- import { CustomerDeal, CustomerDealPersonLink, CustomerEntity } from '../../../../data/entities'
13
+ import {
14
+ CustomerDeal,
15
+ CustomerDealPersonLink,
16
+ CustomerEntity,
17
+ CustomerPersonProfile,
18
+ } from '../../../../data/entities'
14
19
  import { createLogger } from '@open-mercato/shared/lib/logger'
15
20
 
16
21
  const logger = createLogger('customers')
@@ -41,12 +46,36 @@ type DealPersonItem = {
41
46
  kind: 'person'
42
47
  linkedAt: string
43
48
  isPrimary: boolean
49
+ // Added for the shared linked-people card, which renders the same shape the company
50
+ // endpoint already returns. Purely additive: every key above keeps its meaning, so
51
+ // `DealLinkedEntitiesTab` and any third-party consumer are unaffected.
52
+ displayName: string
53
+ primaryEmail: string | null
54
+ primaryPhone: string | null
55
+ status: string | null
56
+ lifecycleStage: string | null
57
+ jobTitle: string | null
58
+ department: string | null
59
+ createdAt: string
60
+ organizationId: string
61
+ temperature: string | null
62
+ source: string | null
44
63
  }
45
64
 
46
65
  function matchesSearch(item: DealPersonItem, query: string): boolean {
47
66
  const normalized = query.trim().toLowerCase()
48
67
  if (!normalized.length) return true
49
- return [item.label, item.subtitle]
68
+ return [
69
+ item.label,
70
+ item.subtitle,
71
+ item.primaryEmail,
72
+ item.primaryPhone,
73
+ item.jobTitle,
74
+ item.department,
75
+ item.status,
76
+ item.lifecycleStage,
77
+ item.source,
78
+ ]
50
79
  .filter((value): value is string => typeof value === 'string' && value.length > 0)
51
80
  .some((value) => value.toLowerCase().includes(normalized))
52
81
  }
@@ -119,17 +148,56 @@ export async function GET(req: Request, ctx: { params?: { id?: string } }) {
119
148
  entityScope,
120
149
  )
121
150
 
151
+ // One batched read for the profile-only columns, keyed by every linked person — not one
152
+ // query per row. The handler filters, sorts and paginates in memory below, so the batch
153
+ // covers the whole link set rather than the page; that is fine at the expected scale of
154
+ // tens of contacts per deal, and the in-memory pipeline is what to revisit first if it
155
+ // ever is not.
156
+ const personIds = links
157
+ .map((link) => (link.person as CustomerEntity | null)?.id)
158
+ .filter((personId): personId is string => typeof personId === 'string' && personId.length > 0)
159
+
160
+ const profiles = personIds.length > 0
161
+ ? await findWithDecryption(
162
+ em,
163
+ CustomerPersonProfile,
164
+ {
165
+ entity: { $in: personIds },
166
+ tenantId: deal.tenantId,
167
+ organizationId: deal.organizationId,
168
+ },
169
+ {},
170
+ entityScope,
171
+ )
172
+ : []
173
+ const profileByPersonId = new Map(
174
+ profiles.map((profile) => [(profile.entity as { id: string }).id, profile]),
175
+ )
176
+
122
177
  const items = links
123
178
  .map((link) => {
124
179
  const person = link.person as CustomerEntity | null
125
180
  if (!person?.id) return null
181
+ const profile = profileByPersonId.get(person.id) ?? null
182
+ const displayName = person.displayName ?? person.primaryEmail ?? person.id
126
183
  return {
127
184
  id: person.id,
128
- label: person.displayName ?? person.primaryEmail ?? person.id,
185
+ label: displayName,
129
186
  subtitle: person.primaryEmail ?? person.primaryPhone ?? null,
130
187
  kind: 'person',
131
188
  linkedAt: link.createdAt.toISOString(),
132
189
  isPrimary: link.isPrimary === true,
190
+ displayName,
191
+ primaryEmail: person.primaryEmail ?? null,
192
+ primaryPhone: person.primaryPhone ?? null,
193
+ status: person.status ?? null,
194
+ lifecycleStage: person.lifecycleStage ?? null,
195
+ jobTitle: profile?.jobTitle ?? null,
196
+ department: profile?.department ?? null,
197
+ createdAt: person.createdAt.toISOString(),
198
+ organizationId: person.organizationId,
199
+ temperature: person.temperature ?? null,
200
+ source: person.source ?? null,
133
201
  } satisfies DealPersonItem
134
202
  })
135
203
  .filter((item): item is DealPersonItem => item !== null)
@@ -176,6 +244,17 @@ export const openApi: OpenApiRouteDoc = {
176
244
  kind: z.literal('person'),
177
245
  linkedAt: z.string(),
178
246
  isPrimary: z.boolean(),
247
+ displayName: z.string(),
248
+ primaryEmail: z.string().nullable(),
249
+ primaryPhone: z.string().nullable(),
250
+ status: z.string().nullable(),
251
+ lifecycleStage: z.string().nullable(),
252
+ jobTitle: z.string().nullable(),
253
+ department: z.string().nullable(),
254
+ createdAt: z.string(),
255
+ organizationId: z.string().uuid().nullable(),
256
+ temperature: z.string().nullable(),
257
+ source: z.string().nullable(),
179
258
  }),
180
259
  ),
181
260
  total: z.number().int().nonnegative(),
@@ -17,6 +17,7 @@ import {
17
17
  } from '@open-mercato/shared/lib/crud/mutation-guard'
18
18
  import { createLogger } from '@open-mercato/shared/lib/logger'
19
19
  import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
20
+ import { calendarEventTypeBehaviorSchema } from '../../../../calendar-event-types'
20
21
 
21
22
  const logger = createLogger('customers')
22
23
 
@@ -30,8 +31,9 @@ const patchSchema = z
30
31
  label: z.string().trim().max(150).optional(),
31
32
  color: z.union([z.string().trim(), z.null()]).optional(),
32
33
  icon: z.union([z.string().trim(), z.null()]).optional(),
34
+ behavior: calendarEventTypeBehaviorSchema.nullable().optional(),
33
35
  })
34
- .refine((input) => input.value !== undefined || input.label !== undefined || input.color !== undefined || input.icon !== undefined, {
36
+ .refine((input) => input.value !== undefined || input.label !== undefined || input.color !== undefined || input.icon !== undefined || input.behavior !== undefined, {
35
37
  message: 'No changes provided',
36
38
  })
37
39
 
@@ -81,6 +83,7 @@ export async function PATCH(req: Request, ctx: { params?: { kind?: string; id?:
81
83
  label: payload.label,
82
84
  color: payload.color,
83
85
  icon: payload.icon,
86
+ behavior: payload.behavior,
84
87
  },
85
88
  ctx: routeContext.ctx,
86
89
  })) as CommandExecuteResult<{ entryId: string; changed: boolean }>
@@ -103,6 +106,7 @@ export async function PATCH(req: Request, ctx: { params?: { kind?: string; id?:
103
106
  ),
104
107
  })
105
108
  }
109
+ if (typeof err.body.code === 'string') throw err
106
110
  throw new CrudHttpError(409, { error: routeContext.translate('customers.config.dictionaries.errors.duplicate', 'An entry with this value already exists') })
107
111
  }
108
112
  if (err.status === 400) {
@@ -141,8 +145,10 @@ export async function PATCH(req: Request, ctx: { params?: { kind?: string; id?:
141
145
  label: entry.label,
142
146
  color: entry.color,
143
147
  icon: entry.icon,
148
+ ...(mappedKind === 'activity_type' ? { behavior: entry.activityTypeBehavior ?? null } : {}),
144
149
  organizationId: entry.organizationId,
145
150
  isInherited: false,
151
+ updatedAt: entry.updatedAt,
146
152
  })
147
153
  if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
148
154
  response.headers.set(
@@ -297,8 +303,10 @@ const dictionaryEntrySchema = z.object({
297
303
  label: z.string().nullable().optional(),
298
304
  color: z.string().nullable().optional(),
299
305
  icon: z.string().nullable().optional(),
306
+ behavior: calendarEventTypeBehaviorSchema.nullable().optional(),
300
307
  organizationId: z.string().uuid().nullable().optional(),
301
308
  isInherited: z.boolean().optional(),
309
+ updatedAt: z.date().or(z.string()).optional(),
302
310
  })
303
311
 
304
312
  const dictionaryDeleteResponseSchema = z.object({
@@ -24,6 +24,8 @@ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
24
24
  import { createLogger } from '@open-mercato/shared/lib/logger'
25
25
  import { CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE } from '../../../lib/dictionaries'
26
26
  import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
27
+ import { calendarEventTypeBehaviorSchema } from '../../../calendar-event-types'
28
+ import type { OrganizationHierarchyService } from '@open-mercato/shared/lib/auth/principal-service'
27
29
 
28
30
  const logger = createLogger('customers')
29
31
 
@@ -35,6 +37,7 @@ const postSchema = z.object({
35
37
  label: z.string().trim().max(150).optional(),
36
38
  color: colorSchema.or(z.null()).optional(),
37
39
  icon: iconSchema.or(z.null()).optional(),
40
+ behavior: calendarEventTypeBehaviorSchema.nullable().optional(),
38
41
  })
39
42
 
40
43
  const querySchema = z.object({
@@ -52,7 +55,7 @@ export async function GET(req: Request, ctx: { params?: { kind?: string } }) {
52
55
  const query = querySchema.parse({
53
56
  organizationId: url.searchParams.get('organizationId') ?? undefined,
54
57
  })
55
- const { translate, em, organizationId, readableOrganizationIds, tenantId, cache } = await resolveDictionaryRouteContext(req, {
58
+ const { translate, em, organizationId, readableOrganizationIds, tenantId, cache, container } = await resolveDictionaryRouteContext(req, {
56
59
  selectedId: query.organizationId ?? undefined,
57
60
  })
58
61
  const { kind, mappedKind } = mapDictionaryKind(ctx.params?.kind)
@@ -64,7 +67,15 @@ export async function GET(req: Request, ctx: { params?: { kind?: string } }) {
64
67
  }
65
68
  const settings = await loadCustomerSettings(em, { tenantId, organizationId })
66
69
  const sortMode = resolveDictionaryEntrySortMode(settings?.dictionarySortModes?.[kind])
67
- const scopedOrganizationIds = readableOrganizationIds.length > 0 ? readableOrganizationIds : [organizationId]
70
+ let scopedOrganizationIds = readableOrganizationIds.length > 0 ? readableOrganizationIds : [organizationId]
71
+ if (mappedKind === 'activity_type') {
72
+ const hierarchy = container.resolve('organizationHierarchyService') as OrganizationHierarchyService
73
+ const ancestors = await hierarchy.resolveAncestorIds({ tenantId, organizationId })
74
+ if (ancestors === null) {
75
+ throw new CrudHttpError(403, { error: translate('customers.errors.organization_forbidden', 'Organization not accessible') })
76
+ }
77
+ scopedOrganizationIds = Array.from(new Set([organizationId, ...ancestors.slice().reverse()]))
78
+ }
68
79
  const canUseCache = Boolean(cache) && mappedKind !== 'person_company_role'
69
80
 
70
81
  let cacheKey: string | null = null
@@ -133,6 +144,7 @@ export async function GET(req: Request, ctx: { params?: { kind?: string } }) {
133
144
  label: entry.label,
134
145
  color: entry.color,
135
146
  icon: entry.icon,
147
+ ...(mappedKind === 'activity_type' ? { behavior: entry.activityTypeBehavior ?? null } : {}),
136
148
  organizationId: entry.organizationId,
137
149
  isInherited: entry.organizationId !== organizationId,
138
150
  createdAt: entry.createdAt,
@@ -213,6 +225,7 @@ export async function POST(req: Request, ctx: { params?: { kind?: string } }) {
213
225
  label: body.label,
214
226
  color: body.color,
215
227
  icon: body.icon,
228
+ behavior: body.behavior,
216
229
  },
217
230
  ctx: context.ctx,
218
231
  })) as CommandExecuteResult<{ entryId: string; mode: 'created' | 'updated' | 'unchanged' }>
@@ -248,8 +261,10 @@ export async function POST(req: Request, ctx: { params?: { kind?: string } }) {
248
261
  label: entry.label,
249
262
  color: entry.color,
250
263
  icon: entry.icon,
264
+ ...(mappedKind === 'activity_type' ? { behavior: entry.activityTypeBehavior ?? null } : {}),
251
265
  organizationId: entry.organizationId,
252
266
  isInherited: false,
267
+ updatedAt: entry.updatedAt,
253
268
  },
254
269
  { status: result.mode === 'created' ? 201 : 200 }
255
270
  )
@@ -288,6 +303,7 @@ const dictionaryEntrySchema = z.object({
288
303
  label: z.string().nullable().optional(),
289
304
  color: z.string().nullable().optional(),
290
305
  icon: z.string().nullable().optional(),
306
+ behavior: calendarEventTypeBehaviorSchema.nullable().optional(),
291
307
  organizationId: z.string().uuid().nullable().optional(),
292
308
  isInherited: z.boolean().optional(),
293
309
  createdAt: z.date().or(z.string()).optional(),
@@ -179,6 +179,7 @@ type InteractionListRow = {
179
179
  appearance_icon: string | null
180
180
  appearance_color: string | null
181
181
  source: string | null
182
+ timezone: string | null
182
183
  duration_minutes: number | null
183
184
  location: string | null
184
185
  all_day: boolean | null
@@ -314,6 +315,7 @@ const INTERACTION_LIST_COLUMNS = [
314
315
  'appearance_icon',
315
316
  'appearance_color',
316
317
  'source',
318
+ 'timezone',
317
319
  'duration_minutes',
318
320
  'location',
319
321
  'all_day',
@@ -713,6 +715,7 @@ export async function GET(req: Request) {
713
715
  source: row.source ?? null,
714
716
  duration: row.duration_minutes ?? null,
715
717
  durationMinutes: row.duration_minutes ?? null,
718
+ timezone: row.timezone ?? null,
716
719
  location: row.location ?? null,
717
720
  allDay: row.all_day ?? null,
718
721
  recurrenceRule: row.recurrence_rule ?? null,
@@ -794,6 +797,7 @@ const interactionListItemSchema = z
794
797
  source: z.string().nullable().optional(),
795
798
  duration: z.number().nullable().optional(),
796
799
  durationMinutes: z.number().nullable().optional(),
800
+ timezone: z.string().nullable().optional(),
797
801
  location: z.string().nullable().optional(),
798
802
  allDay: z.boolean().nullable().optional(),
799
803
  recurrenceRule: z.string().nullable().optional(),
@@ -153,6 +153,7 @@ const todoItemSchema = z.object({
153
153
  todoSeverity: z.string().nullable().optional(),
154
154
  todoDescription: z.string().nullable().optional(),
155
155
  todoDueAt: z.string().nullable().optional(),
156
+ todoTimezone: z.string().nullable().optional(),
156
157
  todoCustomValues: z.record(z.string(), z.unknown()).nullable().optional(),
157
158
  todoOrganizationId: z.string().nullable(),
158
159
  organizationId: z.string(),
@@ -1,3 +1,22 @@
1
+ import React from 'react'
2
+
3
+ const pipelineIcon = React.createElement(
4
+ 'svg',
5
+ {
6
+ width: 16,
7
+ height: 16,
8
+ viewBox: '0 0 24 24',
9
+ fill: 'none',
10
+ stroke: 'currentColor',
11
+ strokeWidth: 2,
12
+ strokeLinecap: 'round',
13
+ strokeLinejoin: 'round',
14
+ },
15
+ React.createElement('rect', { x: 3, y: 4, width: 5, height: 16, rx: 1 }),
16
+ React.createElement('rect', { x: 10, y: 4, width: 5, height: 16, rx: 1 }),
17
+ React.createElement('rect', { x: 17, y: 4, width: 4, height: 16, rx: 1 })
18
+ )
19
+
1
20
  export const metadata = {
2
21
  requireAuth: true,
3
22
  requireFeatures: ['customers.pipelines.manage'],
@@ -6,6 +25,7 @@ export const metadata = {
6
25
  pageGroup: 'Module Configs',
7
26
  pageGroupKey: 'settings.sections.moduleConfigs',
8
27
  pageOrder: 4,
28
+ icon: pipelineIcon,
9
29
  pageContext: 'settings' as const,
10
30
  breadcrumb: [
11
31
  { label: 'Pipeline stages', labelKey: 'customers.config.nav.pipelineStages' },
@@ -42,7 +42,6 @@ type UseDealAssociationsResult = {
42
42
  companiesSaving: boolean
43
43
  handlePeopleAssociationsChange: (nextIds: string[]) => Promise<void>
44
44
  handleCompaniesAssociationsChange: (nextIds: string[]) => Promise<void>
45
- loadLinkedPeoplePage: (page: number, query: string) => Promise<LinkedPageResult>
46
45
  loadLinkedCompaniesPage: (page: number, query: string) => Promise<LinkedPageResult>
47
46
  }
48
47
 
@@ -64,33 +63,6 @@ export function useDealAssociations({
64
63
  setCompaniesEditorIds(data?.linkedCompanyIds ?? [])
65
64
  }, [data?.linkedCompanyIds, data?.linkedPersonIds])
66
65
 
67
- const loadLinkedPeoplePage = React.useCallback(
68
- async (page: number, query: string): Promise<LinkedPageResult> => {
69
- if (!currentDealId) {
70
- return { items: [], totalPages: 1, total: 0 }
71
- }
72
- const params = new URLSearchParams({
73
- page: String(page),
74
- pageSize: '20',
75
- sort: 'name-asc',
76
- })
77
- if (query.trim().length > 0) {
78
- params.set('search', query.trim())
79
- }
80
- const payload = await readApiResultOrThrow<{
81
- items?: DealAssociation[]
82
- total?: number
83
- totalPages?: number
84
- }>(`/api/customers/deals/${encodeURIComponent(currentDealId)}/people?${params.toString()}`)
85
- return {
86
- items: Array.isArray(payload.items) ? payload.items : [],
87
- totalPages: typeof payload.totalPages === 'number' ? payload.totalPages : 1,
88
- total: typeof payload.total === 'number' ? payload.total : 0,
89
- }
90
- },
91
- [currentDealId],
92
- )
93
-
94
66
  const loadLinkedCompaniesPage = React.useCallback(
95
67
  async (page: number, query: string): Promise<LinkedPageResult> => {
96
68
  if (!currentDealId) {
@@ -220,7 +192,6 @@ export function useDealAssociations({
220
192
  companiesSaving,
221
193
  handlePeopleAssociationsChange,
222
194
  handleCompaniesAssociationsChange,
223
- loadLinkedPeoplePage,
224
195
  loadLinkedCompaniesPage,
225
196
  }
226
197
  }
@@ -3,7 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import { extensionPoints } from '@open-mercato/core/modules/customers/extension-points'
5
5
  import Link from 'next/link'
6
- import { Building2, FileText, Hash, UserSearch, Users } from 'lucide-react'
6
+ import { Building2, FileText, Hash, UserSearch } from 'lucide-react'
7
7
  import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
8
8
  import { useRouter, useSearchParams, usePathname } from 'next/navigation'
9
9
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
@@ -24,6 +24,7 @@ import { DealDetailHeader } from '../../../../components/detail/DealDetailHeader
24
24
  import { DealDetailTabs, resolveLegacyTab, type DealTabId } from '../../../../components/detail/DealDetailTabs'
25
25
  import { DealForm, useDealAssociationLookups } from '../../../../components/detail/DealForm'
26
26
  import { DealLinkedEntitiesTab } from '../../../../components/detail/DealLinkedEntitiesTab'
27
+ import { DealPeopleSection } from '../../../../components/detail/DealPeopleSection'
27
28
  import { ConfirmDealLostDialog } from '../../../../components/detail/ConfirmDealLostDialog'
28
29
  import { DealLostSummaryDialog } from '../../../../components/detail/DealLostSummaryDialog'
29
30
  import { DealWonPopup } from '../../../../components/detail/DealWonPopup'
@@ -116,7 +117,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
116
117
  setActiveTab(initialTab)
117
118
  }, [initialTab])
118
119
 
119
- const { searchPeoplePage, fetchPeopleByIds, searchCompaniesPage, fetchCompaniesByIds } = useDealAssociationLookups({
120
+ const { searchCompaniesPage, fetchCompaniesByIds } = useDealAssociationLookups({
120
121
  excludeLinkedDealId: data?.deal.id ?? null,
121
122
  })
122
123
 
@@ -191,7 +192,6 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
191
192
  companiesSaving,
192
193
  handlePeopleAssociationsChange,
193
194
  handleCompaniesAssociationsChange,
194
- loadLinkedPeoplePage,
195
195
  loadLinkedCompaniesPage,
196
196
  } = useDealAssociations({
197
197
  currentDealId,
@@ -534,22 +534,20 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
534
534
 
535
535
  if (activeTab === 'people') {
536
536
  return (
537
- <DealLinkedEntitiesTab
538
- entityLabel={t('customers.deals.detail.tabs.peopleSingular', 'Person')}
539
- entityLabelPlural={t('customers.deals.detail.tabs.people', 'People')}
540
- manageLabel={t('customers.deals.detail.peopleEditorTitle', 'Manage linked people')}
541
- searchPlaceholder={t('customers.deals.detail.peopleSearch', 'Search linked people…')}
542
- linkedItems={data.people}
543
- linkedCount={data.counts.people}
537
+ <DealPeopleSection
538
+ dealId={data.deal.id}
539
+ dealName={dealName}
544
540
  selectedIds={peopleEditorIds}
545
- disabled={peopleSaving || isSaving}
546
- savePending={peopleSaving}
547
- hrefBuilder={(personId) => `/backend/customers/people-v2/${encodeURIComponent(personId)}`}
548
541
  onSaveSelection={(next) => handlePeopleAssociationsChange(next)}
549
- loadLinkedPage={loadLinkedPeoplePage}
550
- searchEntities={searchPeoplePage}
551
- fetchEntitiesByIds={fetchPeopleByIds}
552
- icon={<Users className="size-4" />}
542
+ addActionLabel={t('customers.deals.detail.peopleAdd', 'Add person')}
543
+ disabled={peopleSaving || isSaving}
544
+ emptyLabel={t('customers.deals.detail.peopleEmpty', 'No people linked to this deal yet.')}
545
+ emptyState={{
546
+ title: t('customers.deals.detail.peopleEmptyTitle', 'Link the people involved'),
547
+ actionLabel: t('customers.deals.detail.peopleEmptyAction', 'Add person'),
548
+ }}
549
+ translator={detailTranslator}
550
+ runGuardedMutation={runMutationWithContext}
553
551
  />
554
552
  )
555
553
  }