@open-mercato/core 0.9.0 → 0.9.1-develop.7459.1.99e3accd30

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 (243) 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/catalog/commands/categories.js +1 -1
  9. package/dist/modules/catalog/commands/categories.js.map +2 -2
  10. package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
  11. package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
  12. package/dist/modules/catalog/commands/products.js +52 -17
  13. package/dist/modules/catalog/commands/products.js.map +2 -2
  14. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
  15. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
  16. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
  17. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
  18. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
  19. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
  20. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
  21. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  22. package/dist/modules/communication_channels/lib/inbound-message-origin.js +1 -1
  23. package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
  24. package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
  25. package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
  26. package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
  27. package/dist/modules/communication_channels/lib/use-connect-channel.js.map +2 -2
  28. package/dist/modules/customers/api/activity-types/route.js +151 -0
  29. package/dist/modules/customers/api/activity-types/route.js.map +7 -0
  30. package/dist/modules/customers/api/deals/[id]/people/route.js +58 -5
  31. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  32. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
  33. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
  34. package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
  35. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  36. package/dist/modules/customers/api/interactions/route.js +3 -0
  37. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  38. package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
  39. package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
  40. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
  41. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
  42. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
  43. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
  44. package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
  45. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  46. package/dist/modules/customers/calendar-event-types.js +548 -0
  47. package/dist/modules/customers/calendar-event-types.js.map +7 -0
  48. package/dist/modules/customers/commands/dictionaries.js +70 -10
  49. package/dist/modules/customers/commands/dictionaries.js.map +2 -2
  50. package/dist/modules/customers/commands/interactions.js +187 -22
  51. package/dist/modules/customers/commands/interactions.js.map +3 -3
  52. package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
  53. package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
  54. package/dist/modules/customers/components/DictionarySettings.js +3 -1
  55. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  56. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
  57. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
  58. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
  59. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  60. package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
  61. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  62. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
  63. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
  64. package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
  65. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
  66. package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
  67. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  68. package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
  69. package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
  70. package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
  71. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
  72. package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
  73. package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
  74. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
  75. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  76. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
  77. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
  78. package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
  79. package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
  80. package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
  81. package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
  82. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
  83. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
  84. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
  85. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
  86. package/dist/modules/customers/components/calendar/types.js +1 -0
  87. package/dist/modules/customers/components/calendar/types.js.map +2 -2
  88. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +78 -343
  89. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  90. package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
  91. package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
  92. package/dist/modules/customers/components/detail/DealForm.js +7 -3
  93. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  94. package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
  95. package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
  96. package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
  97. package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
  98. package/dist/modules/customers/components/detail/PersonCard.js +5 -2
  99. package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
  100. package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
  101. package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
  102. package/dist/modules/customers/data/entities.js +6 -0
  103. package/dist/modules/customers/data/entities.js.map +2 -2
  104. package/dist/modules/customers/data/validators.js +39 -10
  105. package/dist/modules/customers/data/validators.js.map +2 -2
  106. package/dist/modules/customers/di.js +20 -1
  107. package/dist/modules/customers/di.js.map +2 -2
  108. package/dist/modules/customers/extension-points.js +13 -0
  109. package/dist/modules/customers/extension-points.js.map +2 -2
  110. package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
  111. package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
  112. package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
  113. package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
  114. package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
  115. package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
  116. package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
  117. package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
  118. package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
  119. package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
  120. package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
  121. package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
  122. package/dist/modules/customers/lib/calendar/timezone.js +111 -0
  123. package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
  124. package/dist/modules/customers/lib/interactionReadModel.js +1 -0
  125. package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
  126. package/dist/modules/customers/lib/todoCompatibility.js +1 -0
  127. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  128. package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
  129. package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
  130. package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
  131. package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
  132. package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
  133. package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
  134. package/dist/modules/messages/subscribers/message-notification.js +13 -1
  135. package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
  136. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
  137. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  138. package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
  139. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  140. package/dist/modules/planner/lib/availabilityMerge.js +101 -38
  141. package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
  142. package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
  143. package/dist/modules/sales/components/documents/LineItemDialog.js +159 -81
  144. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  145. package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
  146. package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
  147. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
  148. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  149. package/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
  150. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  151. package/generated/entities/customer_dictionary_entry/index.ts +1 -0
  152. package/generated/entities/customer_interaction/index.ts +1 -0
  153. package/generated/entity-fields-registry.ts +2 -0
  154. package/package.json +9 -8
  155. package/src/modules/auth/i18n/de.json +3 -0
  156. package/src/modules/auth/i18n/en.json +3 -0
  157. package/src/modules/auth/i18n/es.json +3 -0
  158. package/src/modules/auth/i18n/ko.json +3 -0
  159. package/src/modules/auth/i18n/pl.json +3 -0
  160. package/src/modules/catalog/commands/categories.ts +1 -1
  161. package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
  162. package/src/modules/catalog/commands/products.ts +58 -17
  163. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
  164. package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
  165. package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
  166. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
  167. package/src/modules/communication_channels/i18n/de.json +4 -0
  168. package/src/modules/communication_channels/i18n/en.json +4 -0
  169. package/src/modules/communication_channels/i18n/es.json +4 -0
  170. package/src/modules/communication_channels/i18n/ko.json +4 -0
  171. package/src/modules/communication_channels/i18n/pl.json +4 -0
  172. package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
  173. package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
  174. package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
  175. package/src/modules/customers/api/activity-types/route.ts +154 -0
  176. package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
  177. package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
  178. package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
  179. package/src/modules/customers/api/interactions/route.ts +4 -0
  180. package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
  181. package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
  182. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
  183. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
  184. package/src/modules/customers/calendar-event-types.ts +674 -0
  185. package/src/modules/customers/commands/dictionaries.ts +84 -8
  186. package/src/modules/customers/commands/interactions.ts +244 -21
  187. package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
  188. package/src/modules/customers/components/DictionarySettings.tsx +5 -3
  189. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
  190. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
  191. package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
  192. package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
  193. package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
  194. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
  195. package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
  196. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
  197. package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
  198. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
  199. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
  200. package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
  201. package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
  202. package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
  203. package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
  204. package/src/modules/customers/components/calendar/types.ts +1 -0
  205. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +94 -388
  206. package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
  207. package/src/modules/customers/components/detail/DealForm.tsx +7 -3
  208. package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
  209. package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
  210. package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
  211. package/src/modules/customers/components/detail/types.ts +1 -0
  212. package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -0
  213. package/src/modules/customers/data/entities.ts +8 -1
  214. package/src/modules/customers/data/validators.ts +42 -7
  215. package/src/modules/customers/di.ts +21 -0
  216. package/src/modules/customers/extension-points.ts +13 -0
  217. package/src/modules/customers/i18n/de.json +104 -8
  218. package/src/modules/customers/i18n/en.json +104 -8
  219. package/src/modules/customers/i18n/es.json +104 -8
  220. package/src/modules/customers/i18n/ko.json +104 -8
  221. package/src/modules/customers/i18n/pl.json +104 -8
  222. package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
  223. package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
  224. package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
  225. package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
  226. package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
  227. package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
  228. package/src/modules/customers/lib/calendar/timezone.ts +117 -0
  229. package/src/modules/customers/lib/interactionReadModel.ts +1 -0
  230. package/src/modules/customers/lib/todoCompatibility.ts +2 -0
  231. package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
  232. package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
  233. package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
  234. package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
  235. package/src/modules/messages/subscribers/message-notification.ts +13 -3
  236. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
  237. package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
  238. package/src/modules/planner/lib/availabilityMerge.ts +106 -41
  239. package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
  240. package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
  241. package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
  242. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
  243. package/src/modules/sync_excel/lib/adapters/customers.ts +33 -27
@@ -28,15 +28,30 @@ type GuardedMutationRunner = <T,>(
28
28
  export type CreatedPersonSummary = {
29
29
  id: string
30
30
  displayName: string
31
- companyId: string
32
- companyName: string
31
+ /**
32
+ * Always emitted, but nullable now that the dialog also serves the company-less case.
33
+ *
34
+ * This IS a lost guarantee, not a free widening: on a result type, `string | null` breaks a
35
+ * consumer that previously wrote `const owner: string = created.companyId`. It is safe here
36
+ * only because no in-repo consumer reads either field — the two `onPersonCreated` callers in
37
+ * `CompanyPeopleSection` ignore the argument entirely, and `DealPeopleSection` reads only
38
+ * `id` and `displayName`. Widening a result is not generally safe; do not read this as
39
+ * precedent.
40
+ */
41
+ companyId: string | null
42
+ companyName: string | null
33
43
  }
34
44
 
35
45
  interface CreatePersonDialogProps {
36
46
  open: boolean
37
47
  onClose: () => void
38
- companyId: string
39
- companyName: string
48
+ /**
49
+ * When set, the company field is locked to it and the create command links the person on
50
+ * the same request. Omit it for the ordinary editable company picker — the deal side does,
51
+ * and links the new person to the deal itself afterwards.
52
+ */
53
+ companyId?: string
54
+ companyName?: string
40
55
  runGuardedMutation?: GuardedMutationRunner
41
56
  onPersonCreated?: (created?: CreatedPersonSummary) => void
42
57
  }
@@ -65,7 +80,7 @@ export function CreatePersonDialog({
65
80
  return createPersonFormFields(t)
66
81
  .filter((field) => field.id !== 'addresses')
67
82
  .map((field) => {
68
- if (field.id !== 'companyEntityId') {
83
+ if (field.id !== 'companyEntityId' || !companyId) {
69
84
  return field
70
85
  }
71
86
  return {
@@ -90,7 +105,7 @@ export function CreatePersonDialog({
90
105
  ),
91
106
  } satisfies CrudField
92
107
  })
93
- }, [companyName, t])
108
+ }, [companyId, companyName, t])
94
109
 
95
110
  const groups = React.useMemo(
96
111
  () => createPersonFormGroups(t).filter((group) => group.id !== 'addresses'),
@@ -98,9 +113,7 @@ export function CreatePersonDialog({
98
113
  )
99
114
 
100
115
  const initialValues = React.useMemo<Partial<PersonFormValues>>(
101
- () => ({
102
- companyEntityId: companyId,
103
- }),
116
+ () => (companyId ? { companyEntityId: companyId } : {}),
104
117
  [companyId],
105
118
  )
106
119
 
@@ -121,14 +134,19 @@ export function CreatePersonDialog({
121
134
  ? await runGuardedMutation(operation, payload)
122
135
  : await operation()
123
136
 
124
- flash(t('customers.people.createDialog.success', 'Person created and linked to company'), 'success')
137
+ flash(
138
+ companyId
139
+ ? t('customers.people.createDialog.success', 'Person created and linked to company')
140
+ : t('customers.people.createDialog.successPlain', 'Person created'),
141
+ 'success',
142
+ )
125
143
  const newId = response?.result?.id ?? response?.result?.entityId ?? ''
126
144
  const displayNameFromPayload = typeof payload.displayName === 'string' ? payload.displayName : ''
127
145
  onPersonCreated?.({
128
146
  id: newId,
129
147
  displayName: displayNameFromPayload,
130
- companyId,
131
- companyName,
148
+ companyId: companyId ?? null,
149
+ companyName: companyName ?? null,
132
150
  })
133
151
  onClose()
134
152
  }, [companyId, companyName, onClose, onPersonCreated, organizationId, runGuardedMutation, t])
@@ -148,16 +166,18 @@ export function CreatePersonDialog({
148
166
  <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-[1028px]" onKeyDown={handleKeyDown}>
149
167
  <DialogHeader>
150
168
  <DialogTitle>{t('customers.people.createDialog.title', 'Add new person')}</DialogTitle>
151
- <div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
152
- <Building2 className="size-3.5" />
153
- <span>{companyName}</span>
154
- <span className="text-xs">·</span>
155
- <span>{t('customers.people.createDialog.autoLink', 'auto-linked to company')}</span>
156
- </div>
169
+ {companyId ? (
170
+ <div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
171
+ <Building2 className="size-3.5" />
172
+ <span>{companyName}</span>
173
+ <span className="text-xs">·</span>
174
+ <span>{t('customers.people.createDialog.autoLink', 'auto-linked to company')}</span>
175
+ </div>
176
+ ) : null}
157
177
  </DialogHeader>
158
178
 
159
179
  <CrudForm<PersonFormValues>
160
- key={`${companyId}:${formInstanceKey}`}
180
+ key={`${companyId ?? 'no-company'}:${formInstanceKey}`}
161
181
  embedded
162
182
  entityIds={[E.customers.customer_entity, E.customers.customer_person_profile]}
163
183
  fields={fields}
@@ -1289,8 +1289,12 @@ export function DealForm({
1289
1289
  ownerUserId: parsed.data.ownerUserId && parsed.data.ownerUserId.length
1290
1290
  ? parsed.data.ownerUserId
1291
1291
  : null,
1292
- personIds,
1293
- companyIds,
1292
+ // Only submit the link lists when this form is actually showing them. The deal
1293
+ // detail page hides the associations group and manages links from its People and
1294
+ // Companies tabs instead, so a Details save there would otherwise write back the
1295
+ // ids this form captured when it mounted — re-linking whoever the user has since
1296
+ // unlinked, and re-dating every surviving link row on the way through.
1297
+ ...(showAssociationsGroup ? { personIds, companyIds } : {}),
1294
1298
  }
1295
1299
  const customEntries = collectCustomFieldValues(values, {
1296
1300
  transform: (value) => normalizeCustomFieldSubmitValue(value),
@@ -1300,7 +1304,7 @@ export function DealForm({
1300
1304
  setPending(false)
1301
1305
  }
1302
1306
  },
1303
- [isSubmitting, onSubmit, pending, t],
1307
+ [isSubmitting, onSubmit, pending, showAssociationsGroup, t],
1304
1308
  )
1305
1309
 
1306
1310
  return (
@@ -0,0 +1,256 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Users } from 'lucide-react'
5
+ import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
7
+ import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
8
+ import type { TabEmptyStateConfig, Translator } from './types'
9
+ import {
10
+ LinkedPeopleSection,
11
+ type LinkedPeoplePage,
12
+ type LinkedPeopleSortMode,
13
+ type LinkedPersonSummary,
14
+ } from './LinkedPeopleSection'
15
+ import { CreatePersonDialog, type CreatedPersonSummary } from './CreatePersonDialog'
16
+ import { createPersonLinkAdapter } from '../linking/adapters/personAdapter'
17
+
18
+ type GuardedMutationRunner = <T>(
19
+ operation: () => Promise<T>,
20
+ mutationPayload?: Record<string, unknown>,
21
+ ) => Promise<T>
22
+
23
+ /**
24
+ * `syncDealPeople` still deletes and recreates every link row on a real change, so a surviving
25
+ * participant's `created_at` — surfaced as `linkedAt` — is rewritten by any unlink or inline
26
+ * create. Both of those are exactly what this tab adds, so the two surfaces that read the value
27
+ * are withheld until the set-diff lands: the recency sort would degenerate into an arbitrary
28
+ * label tie-break, and the card would claim a six-month-old contact was linked today.
29
+ *
30
+ * Dropping these two lines is the whole of that follow-up's UI work.
31
+ */
32
+ const DEAL_SORTS: LinkedPeopleSortMode[] = ['name-asc', 'name-desc']
33
+ const DEAL_SHOWS_LINKED_DATE = false
34
+
35
+ export type DealPeopleSectionProps = {
36
+ dealId: string
37
+ dealName?: string | null
38
+ /** Ids currently linked to the deal; also seeds the link dialog selection. */
39
+ selectedIds: string[]
40
+ /** Persists the whole selection through the deal's optimistic-locked update. */
41
+ onSaveSelection: (nextIds: string[]) => Promise<void>
42
+ fallbackPeople?: LinkedPersonSummary[]
43
+ addActionLabel: string
44
+ disabled?: boolean
45
+ emptyLabel: string
46
+ emptyState: TabEmptyStateConfig
47
+ translator?: Translator
48
+ runGuardedMutation?: GuardedMutationRunner
49
+ onLoadingChange?: (isLoading: boolean) => void
50
+ }
51
+
52
+ export function DealPeopleSection({
53
+ dealId,
54
+ dealName,
55
+ selectedIds,
56
+ onSaveSelection,
57
+ fallbackPeople,
58
+ addActionLabel,
59
+ disabled = false,
60
+ emptyLabel,
61
+ emptyState,
62
+ translator,
63
+ runGuardedMutation,
64
+ onLoadingChange,
65
+ }: DealPeopleSectionProps) {
66
+ const tHook = useT()
67
+ const fallbackTranslator = React.useMemo<Translator>(
68
+ () => createTranslatorWithFallback(tHook),
69
+ [tHook],
70
+ )
71
+ const translate: Translator = translator ?? fallbackTranslator
72
+ const [createDialogOpen, setCreateDialogOpen] = React.useState(false)
73
+ const [refreshKey, setRefreshKey] = React.useState(0)
74
+
75
+ const loadPage = React.useCallback(
76
+ async ({
77
+ page,
78
+ pageSize,
79
+ sort,
80
+ search,
81
+ }: {
82
+ page: number
83
+ pageSize: number
84
+ sort: LinkedPeopleSortMode
85
+ search: string
86
+ }): Promise<LinkedPeoplePage> => {
87
+ const params = new URLSearchParams({
88
+ page: String(page),
89
+ pageSize: String(pageSize),
90
+ sort,
91
+ })
92
+ if (search.length > 0) {
93
+ params.set('search', search)
94
+ }
95
+ return readApiResultOrThrow<LinkedPeoplePage>(
96
+ `/api/customers/deals/${encodeURIComponent(dealId)}/people?${params.toString()}`,
97
+ undefined,
98
+ {
99
+ errorMessage: translate(
100
+ 'customers.deals.detail.people.loadError',
101
+ 'Failed to load people.',
102
+ ),
103
+ },
104
+ )
105
+ },
106
+ [dealId, translate],
107
+ )
108
+
109
+ // Both writes go through the deal's own update rather than a per-link endpoint, so they
110
+ // inherit its optimistic lock and the conflict bar the page already renders on a 409.
111
+ const handleUnlink = React.useCallback(
112
+ async (personId: string) => {
113
+ await onSaveSelection(selectedIds.filter((id) => id !== personId))
114
+ },
115
+ [onSaveSelection, selectedIds],
116
+ )
117
+
118
+ const handleLinkConfirm = React.useCallback(
119
+ async ({ nextSelectedIds }: { nextSelectedIds: string[] }) => {
120
+ await onSaveSelection(nextSelectedIds)
121
+ },
122
+ [onSaveSelection],
123
+ )
124
+
125
+ // The people CRUD route can auto-link a new person to a company, but has no deal equivalent,
126
+ // so the deal link is a follow-up write through the same optimistic-locked selection save.
127
+ const handlePersonCreated = React.useCallback(
128
+ async (created?: CreatedPersonSummary) => {
129
+ const createdId = created?.id?.trim()
130
+ if (!createdId || selectedIds.includes(createdId)) {
131
+ setRefreshKey((current) => current + 1)
132
+ return
133
+ }
134
+ try {
135
+ await onSaveSelection([...selectedIds, createdId])
136
+ } catch {
137
+ // onSaveSelection has already reported the failure; the person exists and can be
138
+ // linked manually. Callers invoke this fire-and-forget, so swallowing here is what
139
+ // keeps a reported failure from surfacing again as an unhandled rejection.
140
+ }
141
+ setRefreshKey((current) => current + 1)
142
+ },
143
+ [onSaveSelection, selectedIds],
144
+ )
145
+
146
+ const personLinkAdapter = React.useMemo(
147
+ () =>
148
+ createPersonLinkAdapter({
149
+ dialogTitle: translate('customers.linking.person.dialogTitle', 'Link person'),
150
+ dialogSubtitle: dealName
151
+ ? translate(
152
+ 'customers.linking.person.dialogSubtitleFor',
153
+ 'Link an existing contact to {{name}}',
154
+ { name: dealName },
155
+ )
156
+ : translate(
157
+ 'customers.deals.detail.people.dialogSubtitle',
158
+ 'Link an existing contact to this deal',
159
+ ),
160
+ sectionLabel: translate('customers.linking.person.sectionLabel', 'MATCHING CONTACTS'),
161
+ searchPlaceholder: translate(
162
+ 'customers.linking.person.searchPlaceholder',
163
+ 'Search all people…',
164
+ ),
165
+ searchEmptyHint: translate(
166
+ 'customers.linking.person.searchEmpty',
167
+ 'No matching people found.',
168
+ ),
169
+ selectedEmptyHint: translate(
170
+ 'customers.linking.person.selectedEmpty',
171
+ 'No people selected.',
172
+ ),
173
+ confirmButtonLabel: translate('customers.linking.person.confirmButton', 'Link person'),
174
+ defaultAvatarIcon: <Users className="size-4" />,
175
+ excludeLinkedDealId: dealId,
176
+ addNew: {
177
+ title: translate('customers.linking.person.addNew', 'Add new contact'),
178
+ subtitle: translate(
179
+ 'customers.deals.detail.people.addNewSubtitle',
180
+ 'The new contact is linked to this deal automatically',
181
+ ),
182
+ // Hand the new person back through `onCreated` rather than saving here. That is the
183
+ // dialog's own flow: it merges the option into the draft, focuses it and closes just
184
+ // the nested form, so anything the user had already ticked survives and the whole
185
+ // selection is written once, on confirm. Saving and force-closing from here would
186
+ // discard those ticks silently.
187
+ render: ({ onCreated, onCancel }) => (
188
+ <CreatePersonDialog
189
+ open
190
+ onClose={onCancel}
191
+ runGuardedMutation={runGuardedMutation}
192
+ onPersonCreated={(created) => {
193
+ const createdId = created?.id?.trim()
194
+ if (!createdId) {
195
+ onCancel()
196
+ return
197
+ }
198
+ onCreated({ id: createdId, label: created?.displayName || createdId })
199
+ }}
200
+ />
201
+ ),
202
+ },
203
+ }),
204
+ [dealId, dealName, runGuardedMutation, translate],
205
+ )
206
+
207
+ return (
208
+ <>
209
+ <LinkedPeopleSection
210
+ scopeId={dealId}
211
+ fallbackPeople={fallbackPeople}
212
+ loadPage={loadPage}
213
+ onUnlink={handleUnlink}
214
+ linkAdapter={personLinkAdapter}
215
+ linkedIds={selectedIds}
216
+ onLinkConfirm={handleLinkConfirm}
217
+ refreshKey={refreshKey}
218
+ availableSorts={DEAL_SORTS}
219
+ showLinkedDate={DEAL_SHOWS_LINKED_DATE}
220
+ addActionLabel={addActionLabel}
221
+ onAddPerson={() => setCreateDialogOpen(true)}
222
+ sectionTitle={translate('customers.deals.detail.people.sectionTitle', 'People')}
223
+ sectionSubtitle={translate(
224
+ 'customers.deals.detail.people.sectionSubtitle',
225
+ 'Contacts involved in this deal',
226
+ )}
227
+ searchPlaceholder={translate(
228
+ 'customers.deals.detail.people.searchPlaceholder',
229
+ 'Search by name, role, email...',
230
+ )}
231
+ linkActionLabel={translate(
232
+ 'customers.deals.detail.people.linkAction',
233
+ 'Link existing person',
234
+ )}
235
+ emptyLabel={emptyLabel}
236
+ emptyState={emptyState}
237
+ disabled={disabled}
238
+ translator={translate}
239
+ runGuardedMutation={runGuardedMutation}
240
+ onLoadingChange={onLoadingChange}
241
+ />
242
+
243
+ <CreatePersonDialog
244
+ open={createDialogOpen}
245
+ onClose={() => setCreateDialogOpen(false)}
246
+ runGuardedMutation={runGuardedMutation}
247
+ onPersonCreated={(created) => {
248
+ setCreateDialogOpen(false)
249
+ void handlePersonCreated(created)
250
+ }}
251
+ />
252
+ </>
253
+ )
254
+ }
255
+
256
+ export default DealPeopleSection